init 5S article and refacto pull system

This commit is contained in:
Julien Calixte
2024-12-22 18:31:31 +01:00
parent 9bf151698f
commit 400566b849
32 changed files with 129 additions and 42 deletions

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import SeparatorIcon from '@/icons/SeparatorIcon.vue'
import FlowConclusion from '@/modules/pull-system/article/FlowConclusion.vue'
import FlowHypothesis from '@/modules/pull-system/article/FlowHypothesis.vue'
import FlowIntro from '@/modules/pull-system/article/FlowIntro.vue'
import FlowMultipleSimulation from '@/modules/pull-system/article/FlowMultipleSimulation.vue'
import FlowSetup from '@/modules/pull-system/article/FlowSetup.vue'
import FlowSingleSimulation from '@/modules/pull-system/article/FlowSingleSimulation.vue'
import FeatureSteps from '@/modules/pull-system/feature/FeatureSteps.vue'
import FlowDashboard from '@/modules/pull-system/feature/FlowDashboard.vue'
import SimulationControls from '@/modules/pull-system/simulation/SimulationControls.vue'
import SimulationDashboard from '@/modules/pull-system/simulation/SimulationDashboard.vue'
</script>
<template>
<div class="flow-article">
<h1>Pull system</h1>
<h2>Ekiden (駅伝): long-distance running relay race</h2>
<FlowIntro class="text" />
<SeparatorIcon />
<FlowHypothesis class="text" />
<SeparatorIcon />
<FlowSetup class="text" />
<FlowDashboard class="above" />
<FeatureSteps alias="playground" />
<SimulationControls type="single" class="above" />
<SimulationDashboard />
<FlowSingleSimulation class="text" />
<SimulationControls type="multiple" class="above" />
<SimulationDashboard />
<FlowMultipleSimulation class="text" />
<SeparatorIcon />
<FlowConclusion class="text" />
</div>
</template>
<style lang="scss">
.flow-article {
color: black;
background-color: white;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
max-width: 1080px;
margin: auto;
.text {
font-size: 16pt;
max-width: 650px;
margin: auto;
text-align: justify;
}
.above {
z-index: 1;
padding-top: 1rem;
background-color: white;
}
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div class="flow-conclusion">
<p>
When money and pressure are in the game, fear, uncertainty, and doubt
spread out rapidly. So we rush, as fast as we can, and when a team has
nothing to do, it becomes a disaster: money are being burned.
</p>
<p>
Teams will overproduce. They will do every pieces they can as they are
autonomous: product team will pitch their features, designers will design
every pages, developers will rush to code forgetting why the customer
needs this precise feature. They will then struggle to get these pieces to
the very end when they will transfer them to the others teams. So the fear
of having nothing to give to the next team pushes us to produce just in
case. Bugs lead to more bugs that lead to more bugs. Multitasking will
drastically drop the productivity.
</p>
<p>
But the best way to succeed is to ask: "Is this what you need? How can I
help you?". Just like in Ekiden, handovers are where the real struggles
happen but we're on the same race, together.
</p>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts"></script>
<template>
<div class="flow-hypothesis">
<p>Here our hypothesis:</p>
<ol>
<li>
it takes the same amount of time for each team to complete a task
<span class="meaning">same task time</span>
</li>
<li>teams have no other external dependencies</li>
<li>
teams know exactly what they need to produce their part, they will tag
any defects they found when verifying the feature is good.
</li>
<li>
0 defect policy: the team where the defect appears must rework the
feature.
</li>
<li>release team never fails</li>
<li>there is no limit on how many defects a feature can have.</li>
</ol>
</div>
</template>
<style scoped lang="scss">
ol {
li {
text-align: left;
}
}
</style>

View File

@@ -0,0 +1,13 @@
<template>
<div class="flow-intro">
<h2>What's the fastest way to deliver an app?</h2>
<p>
How good are we to succeed a project? Delivering the features at the good
quality and at the right time. We're pretty bad at it and there is a lot
of misconceptions on how to manage people to do what need to be done. I
wanted to create a simulation to plan ahead and see what are the
consequences of different strategy patterns and which is the most
effective.
</p>
</div>
</template>

View File

@@ -0,0 +1,24 @@
<template>
<div class="flow-conclusion">
<p>
Teams tend to underestimate how long a project will be. And how hard it
will be to work with others.
</p>
<p>
If we're not in a good pace, we just have to try harder. Only once. "Just
in time" becomes "Just this time" many times. So teams overproduce.
Creating stock and latent defects the teams need to rework. The worse the
project do, the more silot we become and we tend to argue with a "I've
done my job, if the project fails it's not my fault.". The fact is that
it's nobody's fault, it's a system.
</p>
<p>
Short term objectives become the norm. It is never the right time to do
problem solving. To think on how we work.
</p>
<p>
Pull system must be a constrain not a choice with the idea to build
around.
</p>
</div>
</template>

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import ProblemSolvingIcon from '@/icons/ProblemSolvingIcon.vue'
import PullSystemIcon from '@/icons/PullSystemIcon.vue'
import PushSystemIcon from '@/icons/PushSystemIcon.vue'
import FeatureItem from '@/modules/pull-system/feature/FeatureItem.vue'
import FlowControls from '@/modules/pull-system/feature/FlowControls.vue'
import QualityIssue from '@/modules/pull-system/feature/QualityIssue.vue'
import { Feature } from '@/modules/pull-system/feature/feature'
const feature: Feature = {
name: 'As a user, in the homepage, I can login',
complexity: 3,
leadTime: 2,
qualityIssue: 4,
status: 'doing',
step: 2
}
</script>
<template>
<div>
<p>
You're a Product Manager in a project who has just started, your goal is
to make a product as fast as you can. With you, you'll have the product
team, designers, developers and a release team.
</p>
<p>This is a feature:</p>
<FeatureItem :feature="feature" />
<p>
<span class="numeric">({{ feature.complexity }})</span> is the complexity
of the feature. The more complex a feature is, the more chance to have
defect we have.
</p>
<p>
<span class="numeric">{{ feature.leadTime }}d</span> is the number of days
the feature started its journey. The ultimate goal is to reduce this
number and deliver as fast as possible.
</p>
<p>
<QualityIssue class="inline" :quality-issue="feature.qualityIssue" />
are the number of defects the feature had during the flow.
</p>
<p>
You have 20 features to deliver, and each day you can choose between 3
strategies:
</p>
<ol>
<li>
Push system
<PushSystemIcon />
</li>
<li>
Pull system
<PullSystemIcon />
</li>
<li>
Problem solving
<ProblemSolvingIcon />
</li>
</ol>
<FlowControls :with-eraser="false" />
<p>
In this article we'll focus on how these strategies are efficient and what
are the impact on the quality the teams produce.
</p>
<h3>The push system: start features as many as possible</h3>
<h4>Seeking for the most of each team</h4>
<p>
Pushing all the feature as fast as possible, this is the only strategy
that allows us to target the best possible result: `# step × # status × #
features`. We may overburden teams in the process though. But as we
already invest money everybody to work so if there is someone who has
nothing to do, this is just money down the drain.
</p>
<h3>The pull system: produce features only when the next team needs it</h3>
<p>
It comes from the assumptions that we will never reach the best score ever
aka "the push system where everything goes right". We know we prefer
waiting for next team to be ready before doing some extra work than having
stock of feature pre-baked.
</p>
<h3>Problem solving: no production, just reflection</h3>
<p>
We invest days where we are not productive at all to investigate and
learning from our mistake. We know that we will never reach the best score
ever and we know that mistakes will reappear. So we take more time to
understand and limit rework. The more the team investigate, the more the
team learn and start to be extremely good at problem solving.
</p>
<h3>Blue bin: the security stock</h3>
<p>
Blue bins are your security stock, they make sure teams can work without
any blockers. The next team will always have material to transform. But it
comes with a cost: too many blue bins add overburden, stagnation
(increasing lead time) and duplicated mistakes. The less you have, the
more the team can focus. But the more you have, the more secure you are to
make teams work, we have to find the good balance.
</p>
</div>
</template>
<style scoped>
.inline {
display: inline;
}
</style>

View File

@@ -0,0 +1,9 @@
<template>
<div class="flow-conclusion">
<p>Pull system wins!</p>
<p>
Let's make sure it wasn't luck and generate 200 projects delivering 200
features each! And see what happens.
</p>
</div>
</template>

View File

@@ -0,0 +1,45 @@
<script setup lang="ts">
import QualityIssue from '@/modules/pull-system/feature/QualityIssue.vue'
import { Feature } from '@/modules/pull-system/feature/feature'
import { computed } from 'vue'
const props = withDefaults(
defineProps<{ feature: Feature; isLive?: boolean }>(),
{ isLive: false }
)
const hasQualityIssues = computed(() => props.feature.qualityIssue > 0)
</script>
<template>
<div class="feature-item bin" :class="{ live: isLive }">
<div>
<span class="numeric">({{ feature.complexity }})</span> {{ feature.name }}
</div>
<div class="numeric">
{{ feature.leadTime }}d
<QualityIssue
v-if="hasQualityIssues"
:quality-issue="feature.qualityIssue"
/>
</div>
</div>
</template>
<style scoped lang="scss">
.feature-item {
transition-property: opacity, visibility;
transition-duration: 0.9s;
transition-timing-function: ease-out;
transition-delay: 1s 2s;
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: space-around;
&.live {
visibility: hidden;
opacity: 0;
}
}
</style>

View File

@@ -0,0 +1,175 @@
<script setup lang="ts">
import FeatureItem from '@/modules/pull-system/feature/FeatureItem.vue'
import { Feature } from '@/modules/pull-system/feature/feature'
import { FeatureStep } from '@/modules/pull-system/feature/feature-steps'
import { useElementSize } from '@vueuse/core'
import { computed, ref } from 'vue'
import { Starport } from 'vue-starport'
const props = defineProps<{
prefix: string
step: FeatureStep
features: Feature[]
}>()
const featuresInProgress = computed(() =>
props.features.filter((feature) => feature.status === 'doing')
)
const featuresDone = computed(() =>
props.features
.filter((feature) => feature.status === 'done')
.sort((a, b) => (a.leadTime > b.leadTime ? -1 : 1))
)
const hasFeaturesInProgress = computed(
() => featuresInProgress.value.length > 0
)
const isLive = computed(
() => props.step.title.toLocaleLowerCase() === 'release'
)
const binContainer = ref<HTMLElement | null>(null)
const { width } = useElementSize(binContainer)
const binContainerWidth = computed(() => {
if (!width) {
return ''
}
return `width: ${width.value}px`
})
</script>
<template>
<li class="feature-step">
<header>{{ step.title }}</header>
<section class="doing">
<h5> [{{ featuresInProgress.length }}]</h5>
<ul v-if="hasFeaturesInProgress">
<li v-for="feature in featuresInProgress" :key="feature.name">
<Starport
:port="`${props.prefix}-${feature.name}`"
style="height: var(--feature-item-height)"
>
<FeatureItem :feature="feature" />
</Starport>
</li>
</ul>
</section>
<section class="done">
<h5>
[{{ featuresDone.length
}}<template v-if="!isLive">/{{ step.blueBins }}</template
>]
</h5>
<div ref="binContainer">
<div
v-if="!isLive"
class="blue-bin-container"
:style="binContainerWidth"
>
<div
v-for="blueBin in step.blueBins"
:key="blueBin"
class="bin blue-bin"
>
blue bin
</div>
</div>
<div v-if="isLive" class="live">
<span v-if="featuresDone.length === 0">No features live yet</span>
<span v-else>
{{ featuresDone.length }} feature<template
v-if="featuresDone.length > 1"
>s</template
>
live!
</span>
</div>
<ul class="done-list">
<li v-for="feature in featuresDone" :key="feature.name">
<Starport
:port="`${props.prefix}-${feature.name}`"
style="height: var(--feature-item-height)"
>
<FeatureItem :feature="feature" :is-live="isLive" />
</Starport>
</li>
</ul>
</div>
</section>
</li>
</template>
<style scoped lang="scss">
@mixin hideScrollbar {
&::-webkit-scrollbar {
width: 0 !important;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
.feature-step {
display: flex;
flex-direction: column;
header {
padding: 0.5rem;
margin-top: 1rem;
}
section {
@include hideScrollbar();
margin: 1rem 0;
flex: 1;
display: flex;
flex-direction: column;
overflow-y: hidden;
max-height: 40vh;
}
h5 {
margin: 0;
background-color: var(--primary-color);
padding: 0.35rem;
text-align: center;
color: var(--color);
}
ul {
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
li {
z-index: 1;
}
}
.bin {
height: var(--feature-item-height);
border: none;
}
.blue-bin-container {
position: absolute;
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 0.5rem;
z-index: 0;
}
/* mobile screen */
@media only screen and (max-width: 750px) {
.blue-bin-container {
display: none;
}
}
.live {
text-align: center;
}
}
</style>

View File

@@ -0,0 +1,62 @@
<script setup lang="ts">
import FeatureStep from '@/modules/pull-system/feature/FeatureStep.vue'
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
import { onMounted } from 'vue'
const NUMBER_OF_FEATURES = 20
defineProps<{ alias: string }>()
const featureStore = useFeatureStore()
onMounted(() => featureStore.initBoard(NUMBER_OF_FEATURES))
</script>
<template>
<ul class="features-steps">
<FeatureStep
v-for="step in featureSteps"
:prefix="alias"
:key="step.title"
:step="step"
:features="featureStore.featuresGroupedByStep[step.stepIndex] ?? []"
/>
</ul>
</template>
<style scoped lang="scss">
.features-steps {
display: flex;
gap: 1rem;
align-items: stretch;
margin: 0 1rem;
border: 3px solid var(--primary-color);
height: 80vh;
width: 100%;
overflow-y: hidden;
li:first-child {
margin-left: 1rem;
}
li:last-child {
margin-right: 1rem;
}
li:not(:last-child) {
padding-right: 1rem;
border-radius: 0;
}
li {
flex: 1;
min-height: 100%;
margin: 0;
color: var(--primary-color);
display: flex;
flex-direction: column;
max-height: 100%;
}
}
</style>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import EraserIcon from '@/icons/EraserIcon.vue'
import ProblemSolvingIcon from '@/icons/ProblemSolvingIcon.vue'
import PullSystemIcon from '@/icons/PullSystemIcon.vue'
import PushSystemIcon from '@/icons/PushSystemIcon.vue'
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
const NUMBER_OF_FEATURES = 20
withDefaults(defineProps<{ withEraser?: boolean }>(), { withEraser: true })
const featureStore = useFeatureStore()
</script>
<template>
<div class="flow-controls">
<div class="row">
<button
@click="featureStore.nextDay('push')"
:disabled="featureStore.isProjectFinished"
>
<PushSystemIcon color="white" />
</button>
<button
@click="featureStore.nextDay('pull')"
:disabled="featureStore.isProjectFinished"
>
<PullSystemIcon color="white" />
</button>
<button
@click="featureStore.nextDay('problem-solving')"
:disabled="featureStore.isProjectFinished"
>
<ProblemSolvingIcon color="white" />
</button>
<button
v-if="withEraser"
@click="featureStore.initBoard(NUMBER_OF_FEATURES)"
>
<EraserIcon color="white" />
</button>
</div>
</div>
</template>
<style scoped lang="scss">
.flow-controls {
.row {
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,106 @@
<script setup lang="ts">
import SeparatorIcon from '@/icons/SeparatorIcon.vue'
import FlowControls from '@/modules/pull-system/feature/FlowControls.vue'
import { useFeatureStore } from '@/modules/pull-system/feature/feature-store'
const featureStore = useFeatureStore()
</script>
<template>
<div class="flow-dashboard">
<SeparatorIcon />
<div class="row cards">
<div class="card">
Features
<span class="numeric">
{{ featureStore.totalFeaturesDone
}}<span class="sub">/{{ featureStore.totalFeaturesCount }} </span>
</span>
</div>
<div class="card">
Team work exp.
<span class="numeric">
{{ featureStore.meta.teamWorkExperience.toFixed(2) }}
</span>
</div>
<div class="card">
Mean complexity
<div class="numeric">{{ featureStore.meanComplexity }}</div>
</div>
<div class="card">
Mean lead time
<div class="data">
<span class="numeric">{{ featureStore.meanLeadTime }}</span>
days
</div>
</div>
</div>
<div class="row cards">
<div class="card">
Takt time
<div class="data">
<span class="numeric">{{ featureStore.taktTime }}</span>
days
</div>
</div>
<div class="card">
Days
<div class="data">
<span class="numeric">{{ featureStore.meta.totalDays }}</span
>d
</div>
</div>
<div class="card">
ETA
<div class="data">
<span class="numeric">{{ featureStore.eat }}</span>
days
</div>
</div>
</div>
<FlowControls />
</div>
</template>
<style scoped lang="scss">
.flow-dashboard {
font-size: 16pt;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
.cards {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
align-items: center;
}
.card {
background-image: linear-gradient(
335deg,
hsl(223deg 58% 88%) 0%,
hsl(224deg 57% 88%) 21%,
hsl(224deg 58% 89%) 30%,
hsl(223deg 57% 89%) 39%,
hsl(222deg 56% 89%) 46%,
hsl(223deg 57% 90%) 54%,
hsl(222deg 55% 90%) 61%,
hsl(223deg 53% 91%) 69%,
hsl(221deg 56% 91%) 79%,
hsl(222deg 53% 92%) 100%
);
padding: 0 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.numeric {
flex: 1;
}
}
}
</style>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
defineProps<{
qualityIssue: number
}>()
</script>
<template>
<div class="quality-issue red-bin numeric">{{ qualityIssue }}</div>
</template>
<style scoped lang="scss">
.quality-issue {
&.red-bin {
--warning-color: #ca0e0e;
border: 2px solid var(--warning-color);
padding: 0 0.5rem 0.1rem;
color: var(--warning-color);
}
}
</style>

View File

@@ -0,0 +1,308 @@
import { Feature, FeatureStatus } from '@/modules/pull-system/feature/feature'
import { FeatureStep } from '@/modules/pull-system/feature/feature-steps'
import { features as initialFeatures } from '@/modules/pull-system/feature/feature.fixture'
import { Strategy } from '@/modules/lean/strategy'
import { FeatureState } from '@/store-type'
import {
getMean,
pickRandomElement,
popNElement,
randomFloat,
shuffleArray,
sumElements
} from '@/utils'
const HARD_STOP = 5000
const getQualityIssue = ({
complexity,
tasksInParallel,
teamWorkExperience
}: {
complexity: number
tasksInParallel: number
teamWorkExperience: number
}): boolean => {
const qualityProbability = getQualityProbability(
complexity,
teamWorkExperience
)
const multiplicator = getOverburdenMultiplicator(tasksInParallel)
const quality = randomFloat(0, 1)
return quality > qualityProbability / multiplicator
}
const mayBeInProgress = ({
features,
feature,
strategy,
steps
}: {
features: Feature[]
feature: Feature
steps: FeatureStep[]
strategy: Strategy
}): FeatureStatus => {
if (strategy !== 'pull') {
return 'doing'
}
const nextStep = steps.find((step) => step.stepIndex === feature.step - 1)
if (!nextStep) {
return feature.status
}
const hasBlueBinAvailableNextStep =
nextStep.blueBins -
features.filter((f) => f.step === feature.step - 1).length >
0
if (hasBlueBinAvailableNextStep) {
return 'doing'
}
return feature.status
}
export const newBacklog = (limit?: number) =>
limit !== undefined
? popNElement(shuffleArray(initialFeatures), limit)
: shuffleArray(initialFeatures)
export const initBoard = (
steps: FeatureStep[],
features: Feature[]
): Feature[] => {
const initialFeatures = popNElement(features, 10)
initialFeatures.forEach((feature) => {
const step = pickRandomElement(steps)
feature.status = pickRandomElement(['doing', 'done'])
feature.step = Math.max(step.stepIndex, 1)
})
return initialFeatures
}
export const getFeaturesForNextDay = ({
backlog,
features,
initialStep,
steps,
strategy,
teamWorkExperience
}: {
backlog: Feature[]
features: Feature[]
steps: FeatureStep[]
initialStep: number
strategy: Strategy | 'problem-solving'
teamWorkExperience: number
}): [Feature[], Feature[]] => {
features
.filter((feature) => feature.step > 0 || feature.status === 'doing')
.sort((a, b) => (a.step < b.step ? -1 : 1))
.forEach((feature) => {
feature.leadTime++
if (strategy === 'problem-solving') {
return
}
switch (feature.status) {
case 'doing':
feature.status = 'done'
break
case 'done':
feature.status = mayBeInProgress({
features,
feature,
steps,
strategy
})
if (feature.status === 'done') {
break
}
const hasQualityIssue = getQualityIssue({
complexity: feature.complexity,
tasksInParallel: features.filter(
(f) => f.status === 'doing' && f.step === feature.step
).length,
teamWorkExperience
})
if (hasQualityIssue) {
feature.qualityIssue++
} else {
// moving to the next team
feature.step--
}
break
}
})
if (backlog.length > 0) {
switch (strategy) {
case 'push': {
const [nextFeature] = popNElement(backlog, 1)
if (nextFeature) {
features.push({ ...nextFeature, step: initialStep })
}
break
}
case 'pull': {
const firstStep = steps.find((step) => step.stepIndex === initialStep)
if (!firstStep) {
break
}
const hasBlueBinAvailableOnFirstStep =
firstStep.blueBins -
features.filter(
(f) => f.step === initialStep && f.status === 'done'
).length >
0
if (hasBlueBinAvailableOnFirstStep) {
const [newFeature] = popNElement(backlog, 1)
if (newFeature) {
features.push({ ...newFeature, step: initialStep })
}
}
}
}
}
return [backlog, features]
}
const getOverburdenMultiplicator = (tasksInParallel: number) => {
switch (tasksInParallel) {
case 1:
return 1
case 2:
return 1.5
case 3:
return 5
case 4:
return 8
case 5:
return 13
default:
return 25
}
}
const getQualityProbability = (
complexity: number,
teamWorkExperience: number
) => {
let probabilityOfGoodQuality = 1
switch (complexity) {
case 1:
probabilityOfGoodQuality = 0.95
case 2:
probabilityOfGoodQuality = 0.88
break
case 3:
probabilityOfGoodQuality = 0.72
break
case 4:
probabilityOfGoodQuality = 0.65
break
case 5:
probabilityOfGoodQuality = 0.5
break
}
// team learning
probabilityOfGoodQuality =
probabilityOfGoodQuality + (1.2 * teamWorkExperience) / 100
return probabilityOfGoodQuality
}
export const isFeatureDone = (feature: Feature) =>
feature.step === 0 && feature.status === 'done'
export const nextDay = (
state: FeatureState,
strategy: Strategy | 'problem-solving'
): FeatureState => {
state.meta.totalDays++
// each day, the teams know how to better work together
state.meta.teamWorkExperience += 0.1
if (strategy === 'problem-solving') {
const hasTeamLearned = randomFloat(0, 1) > 0.25
if (hasTeamLearned) {
state.meta.teamWorkExperience += 1.2
}
} else {
state.meta.strategy[strategy]++
}
const [backlog, features] = getFeaturesForNextDay({
backlog: state.backlog,
features: state.features,
steps: state.steps,
initialStep: state.steps[0].stepIndex,
strategy,
teamWorkExperience: state.meta.teamWorkExperience
})
state.backlog = backlog
state.features = features
const featuresDone = sumElements(state.meta.featuresDonePerDay) ?? 0
const featuresDoneNextDay = features.filter(isFeatureDone).length
state.meta.featuresDonePerDay.push(featuresDoneNextDay - featuresDone)
return state
}
export const isProjectFinished = (features: Feature[]) =>
features.every(isFeatureDone)
export const getMeanComplexity = (features: Feature[]) => {
return getMean(features.map((feature) => feature.complexity))
}
export const getMeanLeadTime = (features: Feature[]) => {
return getMean(features.map((feature) => feature.leadTime))
}
export const getMeanQualityIssue = (features: Feature[]) => {
return getMean(features.map((feature) => feature.qualityIssue))
}
export const simulate = (
state: FeatureState,
strategy: Strategy
): FeatureState => {
let i = 0
while (!isProjectFinished(state.features) && i++ < HARD_STOP) {
if (strategy.includes('dps')) {
if (state.meta.totalDays % 5 === 0) {
state = nextDay(state, 'problem-solving')
} else {
state = nextDay(state, strategy.split('-')[0] as Strategy)
}
} else {
state = nextDay(state, strategy)
}
}
return state
}

View File

@@ -0,0 +1,36 @@
export type FeatureStep = {
title: string
blueBins: number
stepIndex: number
}
const BLUE_BIN_MAX = 2
const featureSteps: FeatureStep[] = [
{
title: 'Product',
stepIndex: 0,
blueBins: BLUE_BIN_MAX
},
{
title: 'Design',
stepIndex: 0,
blueBins: BLUE_BIN_MAX
},
{
title: 'Development',
stepIndex: 0,
blueBins: BLUE_BIN_MAX
},
{
title: 'Release',
stepIndex: 0,
blueBins: 99999
}
]
featureSteps.forEach(
(feature, index) => (feature.stepIndex = featureSteps.length - index - 1)
)
export { featureSteps }

View File

@@ -0,0 +1,88 @@
import { Feature } from '@/modules/pull-system/feature/feature'
import {
getMeanComplexity,
getMeanLeadTime,
getMeanQualityIssue,
initBoard,
isFeatureDone,
isProjectFinished,
newBacklog,
nextDay
} from '@/modules/pull-system/feature/feature-board'
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
import { Strategy } from '@/modules/lean/strategy'
import { FeatureState, Meta } from '@/store-type'
import { defineStore } from 'pinia'
const resetMeta = (): Meta => ({
totalDays: 0,
teamWorkExperience: 0,
featuresDonePerDay: [],
strategy: {
push: 0,
pull: 0,
'pull-dps': 0,
'push-dps': 0
}
})
export const useFeatureStore = defineStore('feature', {
state: (): FeatureState => ({
steps: [],
features: [],
backlog: [],
meta: resetMeta()
}),
actions: {
async initBoard(limit?: number) {
this.backlog = newBacklog(limit)
this.steps = featureSteps
this.features = initBoard(this.steps, this.backlog)
this.backlog = this.backlog.filter(
(l) => !this.features.find((f) => f.name === l.name)
)
this.meta = resetMeta()
},
async nextDay(strategy: Strategy | 'problem-solving') {
const newState = nextDay(this.$state, strategy)
this.backlog = newState.backlog
this.meta = newState.meta
this.features = newState.features
}
},
getters: {
isProjectFinished: (state) => isProjectFinished(state.features),
meanComplexity: (state) => getMeanComplexity(state.features),
meanLeadTime: (state) => getMeanLeadTime(state.features),
meanQualityIssue: (state) => getMeanQualityIssue(state.features),
taktTime: (state): string =>
(
state.meta.totalDays / state.features.filter(isFeatureDone).length || 0
).toFixed(2),
featuresGroupedByStep: (state) => {
const groupedByStep: Record<number, Feature[]> = {}
state.features.forEach((feature) => {
if (!groupedByStep[feature.step]) {
groupedByStep[feature.step] = [feature]
} else {
groupedByStep[feature.step].push(feature)
}
})
return groupedByStep
},
eat(): string {
return (
parseFloat(this.taktTime) *
(this.features.filter((feature) => !isFeatureDone(feature)).length +
this.backlog.length)
).toFixed(2)
},
totalFeaturesCount: (state) => state.backlog.length + state.features.length,
totalFeaturesDone: (state) =>
state.features.filter((feature) => isFeatureDone(feature)).length
}
})

View File

@@ -0,0 +1,10 @@
import { faker } from '@faker-js/faker'
import { describe, it } from 'vitest'
describe('feature fixture', () => {
it('creates lots of animals', () => {
for (let i = 0; i < 200; i++) {
console.log(`"${faker.animal.bird()}", `)
}
})
})

View File

@@ -0,0 +1,12 @@
import { birds } from '@/data/bird'
import { Feature } from '@/modules/pull-system/feature/feature'
import { randomInteger } from '@/utils'
export const features: Feature[] = birds.map((name) => ({
name,
complexity: randomInteger(1, 5),
leadTime: 0,
status: 'doing',
step: Infinity,
qualityIssue: 0
}))

View File

@@ -0,0 +1,10 @@
export type FeatureStatus = 'doing' | 'done'
export type Feature = {
name: string
complexity: number
leadTime: number
status: FeatureStatus
step: number
qualityIssue: number
}

View File

@@ -0,0 +1 @@
export type Strategy = 'push' | 'pull' | 'pull-dps' | 'push-dps'

View File

@@ -0,0 +1,84 @@
<script setup lang="ts">
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
import { popNElement } from '@/utils'
import chartXkcd from 'chart.xkcd'
import { onMounted, ref, watch } from 'vue'
const simulationStore = useSimulationStore()
const SAMPLE = 15
const svgElement = ref<HTMLInputElement | null>(null)
const isChartInit = ref(false)
const reloadGraph = () => {
if (simulationStore.dashboards.length === 0) {
return
}
isChartInit.value = true
if (!svgElement.value) {
return
}
const samples = popNElement(
[...simulationStore.dashboards],
SAMPLE
).toReversed()
const config = {
title: `${SAMPLE} simulations`,
xLabel: 'days',
yLabel: 'features done',
data: {
labels: Array.from(
{
length: Math.max(
...samples.map((dashboard) => dashboard.meta.totalDays)
)
},
(_, index) => index + 1
),
datasets: samples.map((dashboard, index) => ({
label: `${dashboard.analysis.strategy} ${index + 1}`,
data: dashboard.meta.featuresDonePerDay
}))
},
options: {
showLegend: false,
fontFamily: 'Noto Serif'
}
}
new chartXkcd.Line(svgElement.value, config)
}
watch(() => simulationStore.hasSimulationFinished, reloadGraph)
watch(svgElement, reloadGraph)
onMounted(reloadGraph)
</script>
<template>
<div class="simulation-chart">
<div v-if="!isChartInit" class="chart no-init">
Chart appears once every simulations resume
</div>
<svg ref="svgElement" v-else class="chart"></svg>
</div>
</template>
<style scoped lang="scss">
.simulation-chart {
--chart-height: 80vh;
height: var(--chart-height);
}
.chart {
width: 80vw;
height: var(--chart-height);
}
.no-init {
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,61 @@
<script setup lang="ts">
import { Strategy } from '@/modules/lean/strategy'
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
defineProps<{
type: 'single' | 'multiple'
}>()
const simulationStore = useSimulationStore()
const NUMBER_OF_SIMULATION = 200
const strategies: Strategy[] = ['push', 'pull', 'push-dps', 'pull-dps']
const simulateEverything = () => {
strategies.forEach((strategy) =>
simulationStore.multiSimulation(NUMBER_OF_SIMULATION, strategy)
)
}
</script>
<template>
<div class="simulation-controls">
<div class="row" v-if="type === 'single'">
<button
class="button button-outline"
@click="simulationStore.multiSimulation(1, 'push')"
>
simulate push system
</button>
<button
class="button button-outline"
@click="simulationStore.multiSimulation(1, 'pull')"
>
simulate pull system
</button>
<button
class="button button-outline"
@click="simulationStore.multiSimulation(1, 'push-dps')"
>
simulate push with DPS
</button>
<button
class="button button-outline"
@click="simulationStore.multiSimulation(1, 'pull-dps')"
>
simulate pull with DPS
</button>
</div>
<div class="row" v-else-if="type === 'multiple'">
<button class="button button-outline" @click="simulateEverything">
simulate {{ NUMBER_OF_SIMULATION }} simulations for each system
</button>
<button
class="button button-clear"
@click="simulationStore.clearDashboard()"
>
clear dashboard
</button>
</div>
</div>
</template>

View File

@@ -0,0 +1,83 @@
<script setup lang="ts">
import { Strategy } from '@/modules/lean/strategy'
import { useSimulationStore } from '@/modules/pull-system/simulation/simulation-store'
const simulationStore = useSimulationStore()
const strategies: Strategy[] = ['push', 'pull', 'push-dps', 'pull-dps']
</script>
<template>
<div class="simulation-dashboard">
<h3>Dashboard</h3>
<h4>
{{ simulationStore.simulationsDone
}}<span class="sub">/{{ simulationStore.requestedSimulation }}</span>
simulations
</h4>
<table>
<thead>
<tr>
<th>mean values</th>
<th class="numeric">push</th>
<th class="numeric">pull</th>
<th class="numeric">push with problem solving</th>
<th class="numeric">pull with problem solving</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total days</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanTotalDays(strategy) }}
</td>
</tr>
<tr>
<td>Lead time</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanLeadTime(strategy) }}
</td>
</tr>
<tr>
<td>Takt time</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanTaktTime(strategy) }}
</td>
</tr>
<tr>
<td>Complexity</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanComplexity(strategy) }}
</td>
</tr>
<tr>
<td>Quality issue</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanQuality(strategy) }}
</td>
</tr>
<tr>
<td>Team work exp.</td>
<td class="numeric" v-for="strategy in strategies" :key="strategy">
{{ simulationStore.meanTeamWorkExperience(strategy) }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<style scoped lang="scss">
.simulation-dashboard {
color: var(--primary-color);
width: 100%;
table {
padding: 1rem;
}
}
.numeric {
text-align: right;
}
</style>

View File

@@ -0,0 +1,161 @@
import { featureSteps } from '@/modules/pull-system/feature/feature-steps'
import { Strategy } from '@/modules/lean/strategy'
import { Dashboard, Meta } from '@/store-type'
import { getRound } from '@/utils'
import { defineStore } from 'pinia'
type Mean = {
leadTimeSum: number
taktTimeSum: number
complexitySum: number
qualityIssueSum: number
teamWorkExperienceSum: number
totalDaysSum: number
simulations: number
}
type State = {
dashboards: Dashboard[]
requestedSimulation: number
simulationsDone: number
mean: Record<Strategy, Mean>
}
const newMean = (): Mean => ({
leadTimeSum: 0,
taktTimeSum: 0,
complexitySum: 0,
qualityIssueSum: 0,
teamWorkExperienceSum: 0,
totalDaysSum: 0,
simulations: 0
})
const instance = new ComlinkWorker<typeof import('../feature/feature-board')>(
new URL('../feature/feature-board', import.meta.url)
)
const resetMeta = (): Meta => ({
totalDays: 0,
teamWorkExperience: 0,
featuresDonePerDay: [],
strategy: {
push: 0,
pull: 0,
'pull-dps': 0,
'push-dps': 0
}
})
export const useSimulationStore = defineStore('dashboard', {
state: (): State => {
return {
dashboards: [],
requestedSimulation: 0,
simulationsDone: 0,
mean: {
push: newMean(),
pull: newMean(),
'pull-dps': newMean(),
'push-dps': newMean()
}
}
},
actions: {
async simulate(strategy: Strategy) {
const steps = featureSteps
const backlog = await instance.newBacklog()
const features = await instance.initBoard(steps, backlog)
const newState = await instance.simulate(
{
backlog,
steps,
features,
meta: resetMeta()
},
strategy
)
const [worstFeature] = newState.features.sort((a, b) =>
a.qualityIssue > b.qualityIssue ? -1 : 1
)
const dashboard: Dashboard = {
uuid: new Date().getTime().toString(),
meta: newState.meta,
analysis: {
meanComplexity: await instance.getMeanComplexity(newState.features),
meanLeadTime: await instance.getMeanLeadTime(newState.features),
meanQualityIssue: await instance.getMeanQualityIssue(
newState.features
),
worstFeature,
strategy
}
}
this.dashboards.push(dashboard)
this.mean[strategy].leadTimeSum += dashboard.analysis.meanLeadTime
this.mean[strategy].taktTimeSum +=
dashboard.meta.totalDays / newState.features.length
this.mean[strategy].complexitySum += dashboard.analysis.meanComplexity
this.mean[strategy].qualityIssueSum += dashboard.analysis.meanQualityIssue
this.mean[strategy].teamWorkExperienceSum +=
dashboard.meta.teamWorkExperience
this.mean[strategy].totalDaysSum += dashboard.meta.totalDays
this.mean[strategy].simulations++
},
async multiSimulation(simulations: number, strategy: Strategy) {
this.requestedSimulation += simulations
for (let i = 0; i < simulations; i++) {
await this.simulate(strategy)
this.simulationsDone++
}
},
clearDashboard() {
this.dashboards = []
this.mean.push = newMean()
this.mean.pull = newMean()
this.mean['pull-dps'] = newMean()
this.mean['push-dps'] = newMean()
this.simulationsDone = 0
this.requestedSimulation = 0
}
},
getters: {
meanLeadTime: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].leadTimeSum,
state.mean[strategy].simulations
),
meanTaktTime: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].taktTimeSum,
state.mean[strategy].simulations
),
meanComplexity: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].complexitySum,
state.mean[strategy].simulations
),
meanQuality: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].qualityIssueSum,
state.mean[strategy].simulations
),
meanTeamWorkExperience: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].teamWorkExperienceSum,
state.mean[strategy].simulations
),
meanTotalDays: (state) => (strategy: Strategy) =>
getRound(
state.mean[strategy].totalDaysSum,
state.mean[strategy].simulations
),
hasSimulationFinished: (state) =>
state.requestedSimulation > 0 &&
state.requestedSimulation === state.simulationsDone
}
})