Scrum4Me/docs/plans/PBI-84-code-binding-order.md
Janpeter Visser 1de872298d
docs(plans): PBI-84 plan — code als bindende volgorde voor stories & taken (#202)
Goedgekeurd plan voor PBI-84: code wordt de bindende sorteersleutel voor
stories/taken, drag-and-drop herordening verdwijnt. Herzien na multi-model
review (P0/P1/P2) + onderzoek van het plan->onderdelen-mechanisme.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 16:30:53 +02:00

164 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan — `code` wordt bindende volgorde voor stories & taken; drag-and-drop eruit
> **Status:** goedgekeurd 2026-05-14 · gematerialiseerd via Scrum4Me-MCP op de Ubuntu-DB.
> Sprint `S-2026-05-14-code-order` · **PBI-84** · Story **ST-1358** (IN_SPRINT) · Taken **T-992 t/m T-1001** (uitvoervolgorde = sort_order 110).
>
> _v3 — herzien na review (P0/P1/P2) + na onderzoek van het plan→onderdelen-mechanisme._
## Context
Stories en taken zijn nu vrij herordenbaar via drag-and-drop (dnd-kit) in de backlog, het sprint-bord en het sprint-taakpaneel. Die drag schrijft een Float `sort_order` weg via reorder-acties. De volgorde is daardoor "los" en dubbel bepaald: `priority` (14) groepeert, `sort_order` sorteert daarbinnen.
Gewenste situatie: de volgorde van stories en taken is **bindend gekoppeld aan hun `code`** (de bestaande `ST-NNN` / `T-N` identifier die al bij creatie wordt toegekend en al bijna overal zichtbaar is). Drag-and-drop verdwijnt. Het `code`-veld blijft bewerkbaar via de bestaande dialogs — dat is de bewuste, in-scope "aanpas-manier"; er komt géён vervangende herorden-UI. `priority` wordt herijkt als puur team-belang-label dat de bindende volgorde niet meer bepaalt.
Netto: één voorspelbare volgorde = de code. De worker voert stories/taken uit in code-volgorde.
## Beslissingen (uit grill-sessie + review)
1. **Scope:** alleen Story + Taak. PBI's houden drag-and-drop én priority-groepering — volledig ongemoeid.
2. **Sorteersleutel:** `code` is de bindende volgorde voor stories/taken — default-weergave én worker-uitvoering. `priority` bepaalt de bindende volgorde nergens meer. **Uitzondering (P2):** in `components/backlog/story-panel.tsx` blijft een priority-sorteermodus als *niet-persistente leesweergave* (muteert niets, alleen een lens).
3. **Volgnummer = bestaande `code`.** Geen nieuw veld, geen schema-wijziging.
4. **Nummering:** product-breed met gaten (bv. `ST-001, ST-004` binnen één PBI) is prima. Geen wijziging aan code-generatie.
5. **`code` blijft bewerkbaar** via de bestaande dialog-inputs — ongemoeid. "Bindend" = volgorde volgt de code + geen drag meer.
6. **MCP in scope:** zowel de app als de externe `scrum4me-mcp`-repo worden bijgewerkt.
7. **Sprint-bord:** alleen herordenen verdwijnt (story-reorder + taak-reorder); backlog↔sprint membership-drag blijft.
8. **`priority` blijft bewerkbaar** via de edit-dialogs (label, geen bindende volgorde) — priority-afhandeling verandert verder niet.
## Mechanisme: plan → onderdelen
Een plan wordt op **twee manieren** omgezet in PBI + stories + taken; beide moeten voortaan code-volgorde respecteren.
**A. Idea-materialisatie (app, UI-knop).** `actions/ideas.ts``materializeIdeaPlanAction` (~620-799). Leest `Idea.plan_md` — markdown met YAML-frontmatter (schema `lib/schemas/idea.ts` `ideaPlanMdFrontmatterSchema`, parser `lib/idea-plan-parser.ts` `parsePlanMd`). Frontmatter = `{ pbi, stories: [{ …, tasks: [...] }] }`; **volgorde = array-positie**, geen expliciet indexveld. De actie loopt `stories[]`/`tasks[]` in volgorde af in één transactie en kent codes toe via een inline product-brede teller (`nextStoryN++` / `nextTaskN++`). Omdat die teller monotoon meeloopt met de lus is **code-volgorde == plan-volgorde**; `sort_order = parseCodeNumber(code)` behoudt de plan-volgorde dus vanzelf. PBI-creatie hierin blijft ongemoeid (PBI buiten scope).
**B. MCP-flow (Claude Code, ná plan-goedkeuring).** Runbook `docs/runbooks/plan-to-pbi-flow.md`: `create_sprint → create_pbi → create_story → create_task`, los aangeroepen. Elke `create_*`-tool genereert zelf de `code` (product-brede teller); volgorde = aanroep-volgorde → code-volgorde. Er is **geen** worker-job die een plan materialiseert (`IDEA_MAKE_PLAN` schrijft alleen `plan_md`).
Beide paden zetten `sort_order` nu nog los van `code` — dat is precies wat §2 rechttrekt.
## Aanpak
### 1. Sorteersleutel: `sort_order` wordt numerieke spiegel van `code`
`orderBy: { code }` kan niet (string-sortering breekt: `T-1, T-10, T-2`). De bestaande **`sort_order` Float-kolom** blijft daarom de interne numerieke sorteersleutel, maar wordt voortaan afgeleid van `code` i.p.v. via drag/membership gemuteerd. Geen schema-wijziging; `Pbi.sort_order` blijft een echte muteerbare Float.
- Nieuwe pure helper **`parseCodeNumber(code: string): number`** in `lib/code.ts` (gedeeld, non-server): pakt de afsluitende cijferreeks (`/(\d+)$/`) en parse't naar int. Geen cijfers → grote fallback-constante (niet-conforme codes sorteren achteraan).
- Spiegel die helper in de MCP-repo (kleine bewuste duplicaat, conform `lib/job-config.ts`-patroon).
- Regel die overal geldt: **elke story/taak-create en elke `code`-edit zet `sort_order = parseCodeNumber(code)`**. Niets anders mag `Story.sort_order` / `Task.sort_order` schrijven.
### 2. Creatie-paden — `sort_order = parseCodeNumber(code)` [P1: idea-flow + beide mechanisme-paden]
Bereken `sort_order` ná het vaststellen van de definitieve `code`. **Eerst** `rg -n "\.(story|task)\.create" app/ actions/ scrum4me-mcp/src/` om de inventaris te bevestigen; bekende paden:
- `actions/stories.ts``createStoryAction` (binnen `createWithCodeRetry`-callback).
- `actions/tasks.ts``saveTask` (create-tak) en `createTaskAction`.
- **`actions/ideas.ts``materializeIdeaPlanAction` (~723-756)** — pad A. Zet nu `code` via inline template-literal en `sort_order: si + 1` / `ti + 1`. Herstructureer: `const code = …` eerst, dan `sort_order: parseCodeNumber(code)` (= de tellerwaarde). Plan-volgorde blijft zo behouden. **PBI-creatie in deze actie niet aanraken.**
- MCP: `scrum4me-mcp/src/tools/create-story.ts` en `create-task.ts` — pad B. Vervang `(last?.sort_order ?? 0) + 1.0` door `parseCodeNumber(code)`. **Verwijder bovendien de `sort_order`-inputparameter** uit deze twee tool-schema's (anders kan een caller de code-binding omzeilen) en werk de tool-descriptions bij. **`create-pbi.ts` blijft ongemoeid** — PBI buiten scope, mag `sort_order`-input houden.
### 3. Edit-paden — `code`-wijziging hersynchroniseert `sort_order`
- `actions/stories.ts``updateStoryAction` en `actions/tasks.ts``saveTask` (update-tak): als `code` wijzigt, herbereken `sort_order = parseCodeNumber(nieuweCode)`. **`priority`-write blijft ongewijzigd.**
- MCP: bevestig met `rg` dat geen `update_*`-tool de `code` van een bestaande story/taak wijzigt; zo niet → geen MCP edit-werk.
### 4. Sprint-membership ontkoppelen van `sort_order` [P0]
Membership-acties mogen uitsluitend `sprint_id` + `status` schrijven, nooit `sort_order` — anders verliest een story z'n code-afgeleide volgorde zodra hij een sprint in/uit gaat.
- `actions/sprints.ts``createSprintAction` (~regels 440-444): verwijder `sort_order: i + 1` uit de `story.update`-data.
- `actions/sprints.ts``addStoryToSprintAction` (~regel 541): verwijder `sort_order: (last?.sort_order ?? 0) + 1.0` (en de bijbehorende `last`-query).
- Géён wijziging nodig (raken `sort_order` al niet): `commitSprintMembershipAction`, `createSprintWithSelectionAction`, `createSprintWithPbisAction`, `removeStoryFromSprintAction`.
### 5. Drag-and-drop & reorder verwijderen [P0: task-list.tsx toegevoegd]
- Acties: verwijder `reorderStoriesAction` (`actions/stories.ts`), `reorderTasksAction` (`actions/tasks.ts`), `reorderSprintStoriesAction` (`actions/sprints.ts`). **Behoud** `reorderPbisAction` + `updatePbiPriorityAction` (PBI).
- REST: verwijder `app/api/stories/[id]/tasks/reorder/route.ts`.
- Backlog-UI: `components/backlog/story-panel.tsx` en `task-panel.tsx` — dnd-kit eruit (DndContext/SortableContext/handlers), platte lijst renderen. Priority-sorteermodus + priority-filter in `story-panel.tsx` blijven (zie beslissing 2).
- Sprint-bord: `components/sprint/sprint-board-client.tsx` + `sprint-backlog.tsx` — alleen de reorder-binnen-sprint-tak uit `handleDragEnd`/`handleReorder` halen; `addStoryToSprintAction`/`removeStoryFromSprintAction` (membership-drag) blijven.
- **Sprint-taakpaneel: `components/sprint/task-list.tsx`** — heeft volledige taak-reorder-DnD (`reorderTasksAction`-import, `DndContext`/`SortableContext`, `SortableTaskRow` met `useSortable`, `handleDragEnd` met `sprint-task-order` optimistic mutation). Verwijder alle DnD; `SortableTaskRow` wordt een platte rij. **Behoud** de click-based status-toggle (`handleStatusToggle``updateTaskStatusAction`) en de edit-actie — die zijn niet drag-gebaseerd.
- Stores:
- `stores/product-workspace/store.ts``story-order` en `task-order` mutatie-types/handlers eruit; **`pbi-order` blijft**.
- `stores/sprint-workspace/types.ts``OptimisticSprintTaskOrderMutation` (`sprint-task-order`) én `sprint-story-order` + de union-takken eruit.
- `stores/sprint-workspace/store.ts``rollbackMutation`-cases voor `sprint-task-order` en `sprint-story-order` eruit.
- `components/solo/solo-board.tsx`: **ongemoeid** — status-kanban (TO_DO/IN_PROGRESS/DONE), geen `sort_order`-herordening.
### 6. Lees-queries — `priority` uit story/taak-ordering [P1: volledige inventaris]
Story/taak-ordering wordt puur `sort_order` asc (eventueel met `created_at` als tiebreaker). **PBI-keys (`pbi.priority`, `pbi.sort_order`) blijven staan.** Begin met een `rg`-sweep, gebruik onderstaande geverifieerde lijst als minimum:
```
rg -n "orderBy|\.sort\(" app/ lib/ actions/ stores/ scrum4me-mcp/src/ | rg -i "priority"
```
**Story-ordering — `priority` laten vallen:** `app/api/products/[id]/backlog/route.ts:49` · `app/api/pbis/[id]/stories/route.ts:33` · `app/(app)/products/[id]/page.tsx:59` · `app/(mobile)/m/products/[id]/page.tsx:45` · `app/(app)/products/[id]/sprint/[sprintId]/page.tsx:131` · `app/api/products/[id]/claude-context/route.ts:61` · `app/api/products/[id]/next-story/route.ts:26` · `app/api/sprints/[id]/workspace/route.ts:47` · `actions/sprint-runs.ts:91` · `scrum4me-mcp/src/tools/get-claude-context.ts:66` · `scrum4me-mcp/src/tools/wait-for-job.ts:612`
**Taak-ordering — `priority` laten vallen:** `app/(app)/products/[id]/page.tsx:86` · `app/(mobile)/m/products/[id]/page.tsx:72` · `app/(app)/products/[id]/sprint/[sprintId]/page.tsx:75` · `app/api/sprints/[id]/tasks/route.ts:29-32` (`story.sort_order` behouden) · `actions/sprint-runs.ts:88` + in-memory `.sort()` (~164-173: `a.priority - b.priority` voor story/taak weg, `a.pbi.*` behouden) · `lib/solo-workspace-server.ts:49-50` · `scrum4me-mcp/src/tools/get-claude-context.ts:76` · `scrum4me-mcp/src/tools/wait-for-job.ts:609`
**Al compliant (taak al puur op `sort_order`) — niet aanraken:** `backlog/route.ts:66` · `claude-context/route.ts:64` · `next-story/route.ts:29` · `workspace/route.ts:55`
**PBI-ordering — MOET blijven (priority behouden):** `backlog/route.ts:35` · `page.tsx:53` · `m/products/[id]/page.tsx:39` · `sprint/[sprintId]/page.tsx:128`
**Stores:** `stores/sprint-workspace/store.ts` `compareStory`/`compareTask` — eventuele priority-tak verwijderen.
### 7. `BacklogTask` krijgt `code` [P1]
De directe kaart-aanpassing compileert niet zonder dit:
- `stores/product-workspace/types.ts` (~28-37): voeg `code: string | null` toe aan `BacklogTask`.
- `app/api/stories/[id]/tasks/route.ts` (~34-43): `code: true` toevoegen aan de task-`select`.
- `app/api/products/[id]/backlog/route.ts` (~67-76): `code: true` toevoegen aan de task-`select`.
- Eventuele normalisatie die DB-task → `BacklogTask` mapt: `code` meenemen.
### 8. Zichtbaarheid volgnummer
Na stap 7: in `components/backlog/task-panel.tsx` `task.code` doorgeven aan `<BacklogCard code={...}>` (hergebruik bestaande `CodeBadge`). Overige views (backlog-story-kaart, sprint-rijen, sprint-taaklijst, solo-kaart, dialogs) tonen de code al.
### 9. Backfill bestaande data
Eenmalige Prisma-migratie (raw SQL): zet `stories.sort_order` en `tasks.sort_order` = numerieke staart van `code` (`CAST(SUBSTRING(code FROM '[0-9]+$') AS DOUBLE PRECISION)`, `COALESCE` voor niet-numerieke codes). Gevolg: bestaande stories/taken verspringen eenmalig naar code-volgorde — de bedoelde uitkomst. Niet draaien terwijl een PER_TASK-run actief is (SPRINT_BATCH is snapshot-beschermd via `SprintTaskExecution.order`).
### 10. Docs & tests
- `docs/patterns/sort-order.md` herschrijven: float-insertion geldt nog uitsluitend voor PBI; story/taak-`sort_order` is een afgeleide numerieke spiegel van `code`, nooit handmatig/membership-gemuteerd.
- **`docs/runbooks/plan-to-pbi-flow.md` herschrijven:** story/taak-volgorde = code-volgorde (= aanroep-volgorde), niet "priority dan call-order".
- **`lib/idea-prompts/make-plan.md` controleren:** verwijder/verbeter eventuele uitspraken die suggereren dat `priority` de uitvoervolgorde bepaalt (priority = label; array-volgorde = volgorde).
- `docs/api/rest-contract.md`: ordering-omschrijving voor stories/taken bijwerken (code-volgorde i.p.v. `[priority, sort_order]`).
- Nieuwe ADR in `docs/adr/`: "`code` is de bindende volgordesleutel voor stories/taken; priority is label".
- Tests: `__tests__/api/reorder.test.ts` verwijderen; `__tests__/actions/ideas-crud.test.ts` (~478-576, materialisatie) bijwerken; backlog-component-tests + `product-workspace`- en `sprint-workspace`-store-tests bijwerken (drag/`*-order`-mutaties weg); `sprint-tasks`/`next-story`/`sprint-runs`-tests: orderBy-verwachtingen bijwerken. Nieuw: `parseCodeNumber`-unit; create zet `sort_order = numeriek(code)` (incl. idea-materialisatie); code-edit hersynchroniseert; membership-actie laat `sort_order` ongemoeid.
## Kritieke bestanden
**App — wijzigen:** `lib/code.ts` (+`parseCodeNumber`), `actions/stories.ts`, `actions/tasks.ts`, `actions/sprints.ts`, `actions/ideas.ts`, `actions/sprint-runs.ts`, `lib/solo-workspace-server.ts`, `app/api/products/[id]/backlog/route.ts`, `app/api/pbis/[id]/stories/route.ts`, `app/api/products/[id]/claude-context/route.ts`, `app/api/products/[id]/next-story/route.ts`, `app/api/sprints/[id]/tasks/route.ts`, `app/api/sprints/[id]/workspace/route.ts`, `app/api/stories/[id]/tasks/route.ts`, `app/(app)/products/[id]/page.tsx`, `app/(mobile)/m/products/[id]/page.tsx`, `app/(app)/products/[id]/sprint/[sprintId]/page.tsx`, `components/backlog/story-panel.tsx`, `components/backlog/task-panel.tsx`, `components/sprint/sprint-board-client.tsx`, `components/sprint/sprint-backlog.tsx`, `components/sprint/task-list.tsx`, `stores/product-workspace/types.ts`, `stores/product-workspace/store.ts`, `stores/sprint-workspace/types.ts`, `stores/sprint-workspace/store.ts`.
**App — verwijderen:** `app/api/stories/[id]/tasks/reorder/route.ts`.
**MCP:** `scrum4me-mcp/src/tools/create-story.ts`, `create-task.ts` (sort_order-logica + input-param weg), `get-claude-context.ts`, `wait-for-job.ts` (+ kleine `parseCodeNumber`-duplicaat). **Niet:** `create-pbi.ts`.
**Hergebruiken:** `nextSequential`/`generateNext*Code` (`lib/code-server.ts`), `createWithCodeRetry`, `parsePlanMd` (`lib/idea-plan-parser.ts`), `CodeBadge`, `BacklogCard`.
## Verificatie
1. `npm run verify && npm run build` (lint + typecheck + test) — in app én MCP-repo.
2. Handmatig (dev-server): nieuwe story/taak (auto-code) landt op code-positie; `code` editen → herordent; `priority` editen → kleur wijzigt, bindende volgorde níét; priority-sorteermodus in story-panel werkt nog als tijdelijke lens; slepen is weg in backlog, sprint-bord én sprint-taaklijst; story toevoegen/verwijderen aan sprint via drag werkt nog en behoudt code-volgorde.
3. Worker-pad: `GET /api/products/:id/next-story` en `GET /api/sprints/:id/tasks` geven code-volgorde terug; MCP `get_claude_context` / `wait_for_job` idem.
4. Creatie: story/taak via app-actie, idea-materialisatie (`materializeIdeaPlanAction` — plan-volgorde behouden) én MCP → `sort_order` = numeriek(code).
5. Membership: story aan sprint toevoegen/verwijderen → `sort_order` ongewijzigd.
6. Backfill-migratie draaien → bestaande stories/taken in code-volgorde.
## Open implementatie-details (niet-blokkerend)
- `parseCodeNumber`: niet-numerieke code → grote fallback-constante; `created_at`/`id` als tiebreaker bij gelijke numerieke waarde.
- `app/api/sprints/[id]/tasks/route.ts` toont nu een afgeleide `${story.code}.${positie}` — optioneel vervangen door echte `task.code` (nu beschikbaar).
- `sort_order` blijft `Float` en behoudt zijn naam (Float→Int of hernoemen = extra migratie + MCP schema-resync; niet de moeite — wel duidelijk documenteren dat het veld voor story/taak een afgeleide is).
- Idea-materialisatie houdt zijn eigen inline code-teller (niet consolideren met `lib/code-server.ts` — buiten scope).
## Taken-mapping (product Scrum4Me, Ubuntu-DB)
| Taak | sort_order | Plan-§ | Repo |
|---|---|---|---|
| T-992 — `parseCodeNumber`-helper | 1 | §1 | app |
| T-993 — `code` op `BacklogTask`-type + queries | 2 | §7 | app |
| T-994 — create + edit: `sort_order` afgeleid van code | 3 | §2, §3 | app |
| T-995 — sprint-membership ontkoppelen van `sort_order` | 4 | §4 | app |
| T-996 — `priority` uit story/taak-orderings | 5 | §6 | app |
| T-997 — drag-and-drop & reorder verwijderen | 6 | §5 | app |
| T-998 — `code` op backlog-taakkaarten | 7 | §8 | app |
| T-999 — scrum4me-mcp: code-volgorde + priority eruit | 8 | §2, §6 | scrum4me-mcp |
| T-1000 — backfill-migratie | 9 | §9 | app |
| T-1001 — docs & tests | 10 | §10 | app |
> De MCP-`create_story` `sprint_id`-koppeling (de "gap" uit de planfase) is **al** geïmplementeerd buiten deze story om, samen met de DB-pointer-fix (`~/.claude.json` → Ubuntu). Taak T-999 mag die wijziging niet terugdraaien.