docs(T-1016): PB-workflow doc — to-be expliciete state machine
Derde laag van het Product Backlog page workflow-doc (PBI-88 / ST-1363): canonieke state-set met mapping op de as-is werkelijkheid, transitietabel, en het ontwerp van een dunne deriveScreenState()-afleidingslaag bovenop de bestaande PBI-74 stores (geen nieuwe store). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf42609d6d
commit
6d071f70b8
1 changed files with 85 additions and 0 deletions
|
|
@ -167,3 +167,88 @@ stateDiagram-v2
|
||||||
```
|
```
|
||||||
|
|
||||||
`DEMO_MODE` staat bewust niet in het diagram: het is geen knoop in de graaf maar een read-only constraint die over álle states heen ligt.
|
`DEMO_MODE` staat bewust niet in het diagram: het is geen knoop in de graaf maar een read-only constraint die over álle states heen ligt.
|
||||||
|
|
||||||
|
## To-be: expliciete state machine
|
||||||
|
|
||||||
|
> **Doelbeeld — nog niet geïmplementeerd.** Deze sectie beschrijft hoe de impliciete states een expliciet, testbaar model kunnen worden, conform het architectuurvoorstel. Het is een ontwerp, geen weerslag van bestaande code.
|
||||||
|
|
||||||
|
### Canonieke state-set
|
||||||
|
|
||||||
|
Het voorstel noemde zeven states (`NO_SPRINT`, `DRAFT`, `EDITING`, `READY_TO_START`, `ACTIVE`, `CLOSED`, `ERROR`). Tegen de werkelijkheid van dit scherm afgezet blijven er **vier** canonieke states over, plus twee cross-cutting gates:
|
||||||
|
|
||||||
|
| Voorstel-state | Canoniek hier | Mapping op de as-is werkelijkheid |
|
||||||
|
|---|---|---|
|
||||||
|
| `NO_SPRINT` | **`NO_SPRINT`** | `PRODUCT_ACTIVE_NO_SPRINTS` + de "geen actieve sprint"-situatie na `clearActiveSprintAction` |
|
||||||
|
| `DRAFT` | **`DRAFT`** | `SPRINT_DRAFT_PENDING` — 1-op-1 |
|
||||||
|
| `EDITING` | **`EDITING`** | `ACTIVE_SPRINT_DIRTY` — 1-op-1 (ongecommitte membership-wijzigingen) |
|
||||||
|
| `ACTIVE` | **`ACTIVE`** (+ `building`-flag) | `ACTIVE_SPRINT_CLEAN` + `ACTIVE_SPRINT_BUILDING` |
|
||||||
|
| `READY_TO_START` | — *(vervalt)* | Bestaat niet: `createSprintWithSelectionAction` maakt de sprint `status: 'OPEN'` én roept meteen `setActiveSprintInSettings` aan. Er is geen tussenstap "sprint klaar, nog niet gestart" |
|
||||||
|
| `CLOSED` | — *(buiten scope)* | `completeSprintAction` zet `status: 'CLOSED'` vanaf de sprint-execution-page. De switcher kán closed/archived sprints tónen ("Toon afgeronde sprints"), maar muteert ze hier niet |
|
||||||
|
| `ERROR` | — *(niet gemodelleerd)* | Server-actions returnen `{ error, code }`; de client toont een `toast.error` en blijft in de huidige state. Er is geen expliciete ERROR-schermtoestand |
|
||||||
|
|
||||||
|
`building` is een **orthogonale flag** op `ACTIVE`/`EDITING` (een `SprintRun` is `QUEUED`/`RUNNING`), geen aparte state — de UI blokkeert tijdens building niets, het is puur een badge.
|
||||||
|
|
||||||
|
**Cross-cutting gates** (geen knopen in de machine, wel bepalend voor wat zichtbaar is):
|
||||||
|
|
||||||
|
- `PRODUCT_NOT_ACTIVE` — `isActiveProduct === false`: de sprint-workflow is nog niet begonnen; alleen `ActivateProductButton` brengt je verder.
|
||||||
|
- `DEMO_MODE` — `session.isDemo`: read-only over alle states.
|
||||||
|
|
||||||
|
### Transitietabel
|
||||||
|
|
||||||
|
`currentState + event + context → nextState`:
|
||||||
|
|
||||||
|
| currentState | event | context / guard | nextState |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `NO_SPRINT` | `OPEN_DRAFT` | — | `DRAFT` |
|
||||||
|
| `DRAFT` | `CANCEL_DRAFT` | — | vorige state (`NO_SPRINT` of `ACTIVE`) |
|
||||||
|
| `DRAFT` | `CREATE_SPRINT` | ≥1 eligible story | `ACTIVE` |
|
||||||
|
| `ACTIVE` | `OPEN_DRAFT` | — | `DRAFT` |
|
||||||
|
| `ACTIVE` | `TOGGLE_MEMBERSHIP` | — | `EDITING` |
|
||||||
|
| `EDITING` | `TOGGLE_MEMBERSHIP` | `pending` wordt leeg | `ACTIVE` |
|
||||||
|
| `EDITING` | `TOGGLE_MEMBERSHIP` | `pending` blijft gevuld | `EDITING` |
|
||||||
|
| `EDITING` | `COMMIT_MEMBERSHIP` | — | `ACTIVE` |
|
||||||
|
| `ACTIVE` / `EDITING` | `SWITCH_SPRINT` | — | `ACTIVE` (andere sprint) |
|
||||||
|
| `ACTIVE` / `EDITING` | `CLEAR_ACTIVE_SPRINT` | — | `NO_SPRINT` |
|
||||||
|
| `ACTIVE` / `EDITING` | `SPRINT_RUN_STARTED` / `_FINISHED` | — | idem state, `building` flag om |
|
||||||
|
|
||||||
|
`CANCEL_DRAFT` is context-afhankelijk: open je `DRAFT` vanuit `ACTIVE`, dan keert annuleren terug naar `ACTIVE`; vanuit `NO_SPRINT` naar `NO_SPRINT`.
|
||||||
|
|
||||||
|
### Dunne afleidingslaag — `deriveScreenState()`
|
||||||
|
|
||||||
|
Conform de met de gebruiker afgestemde keuze blijven de **PBI-74 bounded-context stores leidend**. Er komt dus **geen `workflowStore`** en geen herstructurering — alleen een dunne, pure afleidingslaag bovenop wat er al is: één functie die de verspreide flags consolideert tot één `ScreenState`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// stores/product-workspace/screen-state.ts — ONTWERP, nog niet geïmplementeerd
|
||||||
|
export type ScreenState =
|
||||||
|
| { kind: 'NO_SPRINT' }
|
||||||
|
| { kind: 'DRAFT' }
|
||||||
|
| { kind: 'ACTIVE'; building: boolean }
|
||||||
|
| { kind: 'EDITING'; building: boolean }
|
||||||
|
|
||||||
|
export interface ScreenStateInput {
|
||||||
|
// SSR-props uit page.tsx
|
||||||
|
activeSprintItem: { id: string } | null
|
||||||
|
buildingSprintIds: string[]
|
||||||
|
// store-slices (geen nieuwe state — alleen lezen)
|
||||||
|
hasPendingDraft: boolean // user-settings: pendingSprintDraft[productId]
|
||||||
|
pendingAdds: string[] // product-workspace: sprintMembership.pending.adds
|
||||||
|
pendingRemoves: string[] // product-workspace: sprintMembership.pending.removes
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deriveScreenState(i: ScreenStateInput): ScreenState {
|
||||||
|
if (i.hasPendingDraft) return { kind: 'DRAFT' } // draft wint van alles
|
||||||
|
if (i.activeSprintItem) {
|
||||||
|
const building = i.buildingSprintIds.includes(i.activeSprintItem.id)
|
||||||
|
const dirty = i.pendingAdds.length > 0 || i.pendingRemoves.length > 0
|
||||||
|
return dirty ? { kind: 'EDITING', building } : { kind: 'ACTIVE', building }
|
||||||
|
}
|
||||||
|
return { kind: 'NO_SPRINT' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ontwerp-uitgangspunten:
|
||||||
|
|
||||||
|
- **Pure functie, geen nieuwe store.** Leest uitsluitend uit de bestaande `product-workspace`- en `user-settings`-stores plus de SSR-props. Geen gedupliceerde state.
|
||||||
|
- **Eén plek voor "in welke state zit ik".** Vandaag zit die afleiding verspreid over `page.tsx`, `sprint-switcher.tsx`, `new-sprint-trigger.tsx` en `save-sprint-button.tsx`; componenten schakelen straks over op één `switch (screenState.kind)`.
|
||||||
|
- **Leeft als selector/util** naast `stores/product-workspace/selectors.ts` — testbaar in isolatie (pure input → output).
|
||||||
|
- `PRODUCT_NOT_ACTIVE` en `DEMO_MODE` blijven **buiten** `ScreenState`: het zijn gates die de UI al apart als booleans heeft, geen knopen in de machine.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue