From 734c17fd2973045df43f0f3aabe27bbddde033bd Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Mon, 24 Jul 2023 22:46:31 +0200 Subject: [PATCH] extract and fix to be able to have a web worker --- src/modules/feature/feature-board.ts | 194 +++++++++++++++++++-------- src/modules/feature/feature-steps.ts | 2 +- src/modules/feature/store.ts | 144 ++++++-------------- 3 files changed, 182 insertions(+), 158 deletions(-) diff --git a/src/modules/feature/feature-board.ts b/src/modules/feature/feature-board.ts index 655c2d6..f17b5d8 100644 --- a/src/modules/feature/feature-board.ts +++ b/src/modules/feature/feature-board.ts @@ -1,10 +1,17 @@ import { Feature } from '@/modules/feature/feature' import { FeatureStep } from '@/modules/feature/feature-steps' -import { features } from '@/modules/feature/feature.fixture' +import { features as initialFeatures } from '@/modules/feature/feature.fixture' +import { State } from '@/modules/feature/store-type' import { Strategy } from '@/modules/lean/strategy' -import { pickRandomElement, popNElement, shuffleArray } from '@/utils' +import { + pickRandomElement, + popNElement, + shuffleArray, + sumElements +} from '@/utils' const MAX_FEATURES = 30 +const HARD_STOP = 5000 const hasQualityIssue = ({ complexity, @@ -26,7 +33,7 @@ const hasQualityIssue = ({ return quality > qualityProbability / multiplicator } -export const newBoard = () => shuffleArray(features) +export const newBoard = () => shuffleArray(initialFeatures) export const initBoard = ( steps: FeatureStep[], @@ -43,7 +50,7 @@ export const initBoard = ( return initialFeatures } -export const nextDay = ({ +export const getFeaturesForNextDay = ({ backlog, features, initialStep, @@ -57,65 +64,62 @@ export const nextDay = ({ initialStep: number strategy: Strategy daysWithProblemSolving: number -}): Feature[] => { - features.forEach((feature) => { - const isFeatureLive = feature.step === 0 && feature.status === 'done' - if (isFeatureLive) { - return - } +}): [Feature[], Feature[]] => { + features + .filter((feature) => feature.step > 0 || feature.status === 'doing') + .forEach((feature) => { + feature.leadTime++ - feature.leadTime++ + if (strategy === 'problem-solving') { + return + } - if (strategy === 'problem-solving') { - return - } + switch (feature.status) { + case 'doing': + feature.status = 'done' + break + case 'done': + if (strategy === 'pull') { + const nextStep = steps.find( + (step) => step.stepIndex === feature.step - 1 + ) + if (!nextStep) { + break + } - switch (feature.status) { - case 'doing': - feature.status = 'done' - break - case 'done': - if (strategy === 'pull') { - const nextStep = steps.find( - (step) => step.stepIndex === feature.step - 1 - ) - if (!nextStep) { - break - } + const hasBlueBinAvailableNextStep = + nextStep.blueBins - + features.filter( + (f) => f.step === feature.step - 1 && f.status === 'done' + ).length > + 0 - const hasBlueBinAvailableNextStep = - nextStep.blueBins - - features.filter( - (f) => f.step === feature.step - 1 && f.status === 'done' - ).length > - 0 - - if (hasBlueBinAvailableNextStep) { + if (hasBlueBinAvailableNextStep) { + feature.status = 'doing' + } + } else { feature.status = 'doing' } - } else { - feature.status = 'doing' - } - if (feature.status === 'doing') { - if ( - hasQualityIssue({ - complexity: feature.complexity, - tasksInParallel: features.filter( - (f) => f.status === 'doing' && f.step === feature.step - ).length, - daysWithProblemSolving - }) - ) { - feature.step = Math.min(4, feature.step + 1) - feature.qualityIssue++ - } else { - feature.step-- + if (feature.status === 'doing') { + if ( + hasQualityIssue({ + complexity: feature.complexity, + tasksInParallel: features.filter( + (f) => f.status === 'doing' && f.step === feature.step + ).length, + daysWithProblemSolving + }) + ) { + feature.step = Math.min(4, feature.step + 1) + feature.qualityIssue++ + } else { + feature.step-- + } } - } - break - } - }) + break + } + }) if (features.length < MAX_FEATURES) { switch (strategy) { @@ -151,7 +155,7 @@ export const nextDay = ({ } } - return features + return [backlog, features] } const getOverburdenMultiplicator = (tasksInParallel: number) => { @@ -201,9 +205,85 @@ const getQualityProbability = ( break } - // learnings + // team learning probabilityOfGoodQuality = probabilityOfGoodQuality + (1.2 * daysWithProblemSolving) / 100 return probabilityOfGoodQuality } + +export const nextDay = (state: State, strategy: Strategy): State => { + state.meta.totalDays++ + state.meta.strategy[strategy]++ + + if (strategy === 'problem-solving') { + state.meta.daysWithProblemSolving++ + } + + const [backlog, features] = getFeaturesForNextDay({ + backlog: state.backlog, + features: state.features, + steps: state.steps, + initialStep: state.steps[0].stepIndex, + strategy, + daysWithProblemSolving: state.meta.daysWithProblemSolving + }) + + state.backlog = backlog + state.features = features + + console.log(state.features) + + return state +} + +export const isProjectFinished = (features: Feature[]) => + features.every((feature) => feature.step === 0 && feature.status === 'done') + +export const meanComplexity = (features: Feature[]) => { + return ( + Math.round( + 100 * + (sumElements(features.map((feature) => feature.complexity)) / + features.length) + ) / 100 + ) +} + +export const meanLeadTime = (features: Feature[]) => { + return ( + Math.round( + 100 * + (sumElements(features.map((feature) => feature.leadTime)) / + features.length) + ) / 100 + ) +} + +export const meanQualityIssue = (features: Feature[]) => { + return ( + Math.round( + 100 * + (sumElements(features.map((feature) => feature.qualityIssue)) / + features.length) + ) / 100 + ) +} + +export const simulate = (state: State, strategy: Strategy): State => { + let i = 0 + + while (!isProjectFinished(state.features) && i++ < HARD_STOP) { + if (strategy === 'problem-solving') { + if (state.meta.totalDays % 5 === 0) { + state = nextDay(state, 'problem-solving') + } else { + state = nextDay(state, 'pull') + } + } else { + state = nextDay(state, strategy) + } + } + + return state +} diff --git a/src/modules/feature/feature-steps.ts b/src/modules/feature/feature-steps.ts index bc5c365..119dae6 100644 --- a/src/modules/feature/feature-steps.ts +++ b/src/modules/feature/feature-steps.ts @@ -30,6 +30,6 @@ export const featureSteps: FeatureStep[] = [ { title: 'Release', stepIndex: 0, - blueBins: Infinity + blueBins: 99999 } ] diff --git a/src/modules/feature/store.ts b/src/modules/feature/store.ts index a4c1d89..ee72d77 100644 --- a/src/modules/feature/store.ts +++ b/src/modules/feature/store.ts @@ -1,37 +1,19 @@ import { Feature } from '@/modules/feature/feature' -import { initBoard, newBoard, nextDay } from '@/modules/feature/feature-board' -import { FeatureStep, featureSteps } from '@/modules/feature/feature-steps' +import { + meanComplexity, + meanLeadTime, + meanQualityIssue +} from '@/modules/feature/feature-board' +import { featureSteps } from '@/modules/feature/feature-steps' +import { Meta, State } from '@/modules/feature/store-type' import { Strategy } from '@/modules/lean/strategy' -import { sumElements } from '@/utils' import { defineStore } from 'pinia' -type Meta = { - totalDays: number - daysWithProblemSolving: number - strategy: Record -} +const clone = (data: any) => JSON.parse(JSON.stringify(data)) -type Analysis = { - worstFeature: Feature - meanQualityIssue: number - meanComplexity: number - meanLeadTime: number - mainStrategy: Strategy | string -} - -type Dashboard = Array<{ - uuid: string - meta: Meta - analysis: Analysis -}> - -type State = { - steps: FeatureStep[] - features: Feature[] - backlog: Feature[] - meta: Meta - dashboards: Dashboard -} +const instance = new ComlinkWorker( + new URL('./feature-board', import.meta.url) +) const resetMeta = (): Meta => ({ totalDays: 0, @@ -52,94 +34,60 @@ export const useFeatureStore = defineStore('feature', { dashboards: [] }), actions: { - initBoard() { - this.backlog = newBoard() + async initBoard() { + const newBoard = await instance.newBoard() + + this.backlog = newBoard this.steps = featureSteps - this.features = initBoard(this.steps, this.backlog) + this.features = await instance.initBoard( + clone(this.steps), + clone(this.backlog) + ) + + this.backlog = this.backlog.filter( + (l) => !this.features.find((f) => f.name === l.name) + ) this.meta = resetMeta() }, - nextDay(strategy: Strategy) { - this.meta.totalDays++ - this.meta.strategy[strategy]++ + async nextDay(strategy: Strategy) { + const newState = await instance.nextDay(clone(this.$state), strategy) - if (strategy === 'problem-solving') { - this.meta.daysWithProblemSolving++ - } - - this.features = nextDay({ - backlog: this.backlog, - features: this.features, - steps: this.steps, - initialStep: this.steps[0].stepIndex, - strategy, - daysWithProblemSolving: this.meta.daysWithProblemSolving - }) + this.backlog = newState.backlog + this.meta = newState.meta + this.features = newState.features }, - simulate(strategy: Strategy) { - this.initBoard() - while (!this.isProjectFinished) { - if (strategy === 'problem-solving') { - if (this.meta.totalDays % 5 === 0) { - this.nextDay('problem-solving') - } else { - this.nextDay('pull') - } - } else { - this.nextDay(strategy) - } - } - const [worstFeature] = this.features.sort((a, b) => + async simulate(strategy: Strategy) { + await this.initBoard() + + const newState = await instance.simulate(clone(this.$state), strategy) + + const [worstFeature] = newState.features.sort((a, b) => a.qualityIssue > b.qualityIssue ? -1 : 1 ) this.dashboards.push({ uuid: new Date().getTime().toString(), - meta: this.meta, + meta: newState.meta, analysis: { - meanComplexity: this.meanComplexity, - meanLeadTime: this.meanLeadTime, - meanQualityIssue: this.meanQualityIssue, + meanComplexity: await instance.meanComplexity(newState.features), + meanLeadTime: await instance.meanLeadTime(newState.features), + meanQualityIssue: await instance.meanQualityIssue(newState.features), worstFeature, - mainStrategy: Object.entries(this.meta.strategy).sort((a, b) => + mainStrategy: Object.entries(newState.meta.strategy).sort((a, b) => a[1] > b[1] ? -1 : 1 )[0][0] } }) + await this.initBoard() }, clearDashboard() { this.dashboards = [] } }, getters: { - meanComplexity: (state) => { - return ( - Math.round( - 100 * - (sumElements(state.features.map((feature) => feature.complexity)) / - state.features.length) - ) / 100 - ) - }, - meanLeadTime: (state) => { - return ( - Math.round( - 100 * - (sumElements(state.features.map((feature) => feature.leadTime)) / - state.features.length) - ) / 100 - ) - }, - meanQualityIssue: (state) => { - return ( - Math.round( - 100 * - (sumElements( - state.features.map((feature) => feature.qualityIssue) - ) / - state.features.length) - ) / 100 - ) - }, + meanComplexity: (state) => meanComplexity(state.features), + meanLeadTime: (state) => meanLeadTime(state.features), + meanQualityIssue: (state) => meanQualityIssue(state.features), featuresGroupedByStep: (state) => { const groupedByStep: Record = {} @@ -152,10 +100,6 @@ export const useFeatureStore = defineStore('feature', { }) return groupedByStep - }, - isProjectFinished: (state) => - state.features.every( - (feature) => feature.step === 0 && feature.status === 'done' - ) + } } })