From 3a7141114c55d72c53663d0dd545eece9a39b254 Mon Sep 17 00:00:00 2001 From: Scrum4Me Agent <30029041+madhura68@users.noreply.github.com> Date: Thu, 14 May 2026 18:13:42 +0200 Subject: [PATCH] docs+tests(sort-order): update for code-binding order on stories/tasks - Rewrite docs/patterns/sort-order.md: float-insertion PBI only; story/task sort_order = parseCodeNumber(code), never drag/membership mutated - Update plan-to-pbi-flow.md: sort_order auto, sprint_id param, priority=label - Update make-plan.md: priority=label, array order = execution order - Update rest-contract.md: fix sprint-tasks ordering, remove reorder endpoint - Add ADR-0011: code is bindende volgordesleutel voor stories/taken - Regenerate docs/INDEX.md via npm run docs - Remove reorderStoriesAction/reorderTasksAction mocks from backlog tests - Remove dnd-kit mocks from task-panel test (panel no longer uses dnd) - Extend materializeIdeaPlanAction test: assert sort_order=parseCodeNumber(code) Co-Authored-By: Claude Sonnet 4.6 --- __tests__/actions/ideas-crud.test.ts | 11 ++- .../components/backlog/integration.test.tsx | 4 +- .../components/backlog/task-panel.test.tsx | 37 --------- docs/INDEX.md | 3 +- ...0011-code-volgordesleutel-stories-taken.md | 58 ++++++++++++++ docs/api/rest-contract.md | 17 +--- docs/patterns/sort-order.md | 77 ++++++++++++++++--- docs/runbooks/plan-to-pbi-flow.md | 11 +-- lib/idea-prompts/make-plan.md | 8 +- 9 files changed, 150 insertions(+), 76 deletions(-) create mode 100644 docs/adr/0011-code-volgordesleutel-stories-taken.md diff --git a/__tests__/actions/ideas-crud.test.ts b/__tests__/actions/ideas-crud.test.ts index a19c663..bf1ba41 100644 --- a/__tests__/actions/ideas-crud.test.ts +++ b/__tests__/actions/ideas-crud.test.ts @@ -520,7 +520,7 @@ body .mockResolvedValueOnce({ id: 't-B1' }) }) - it('happy: creates PBI + 2 stories + 3 tasks, links idea, returns ids', async () => { + it('happy: creates PBI + 2 stories + 3 tasks, links idea, returns ids; sort_order = parseCodeNumber(code)', async () => { const r = await materializeIdeaPlanAction('idea-1') expect(r).toMatchObject({ success: true, @@ -534,6 +534,15 @@ body expect(m.pbi.create).toHaveBeenCalledTimes(1) expect(m.story.create).toHaveBeenCalledTimes(2) expect(m.task.create).toHaveBeenCalledTimes(3) + + // story sort_order = parseCodeNumber(auto-code): ST-001→1, ST-002→2 + expect(m.story.create.mock.calls[0][0].data.sort_order).toBe(1) + expect(m.story.create.mock.calls[1][0].data.sort_order).toBe(2) + + // task sort_order = parseCodeNumber(auto-code): T-1→1, T-2→2, T-3→3 + expect(m.task.create.mock.calls[0][0].data.sort_order).toBe(1) + expect(m.task.create.mock.calls[1][0].data.sort_order).toBe(2) + expect(m.task.create.mock.calls[2][0].data.sort_order).toBe(3) }) it('blocks when not PLAN_READY (e.g. GRILLED)', async () => { diff --git a/__tests__/components/backlog/integration.test.tsx b/__tests__/components/backlog/integration.test.tsx index 7875c50..0a41b94 100644 --- a/__tests__/components/backlog/integration.test.tsx +++ b/__tests__/components/backlog/integration.test.tsx @@ -25,18 +25,16 @@ Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, wri // Mock server actions vi.mock('@/actions/stories', () => ({ - reorderStoriesAction: vi.fn().mockResolvedValue({ success: true }), reorderPbisAction: vi.fn().mockResolvedValue({ success: true }), updatePbiPriorityAction: vi.fn().mockResolvedValue({ success: true }), })) vi.mock('@/actions/pbis', () => ({ deletePbiAction: vi.fn().mockResolvedValue({ success: true }) })) -vi.mock('@/actions/tasks', () => ({ reorderTasksAction: vi.fn().mockResolvedValue({ success: true }) })) vi.mock('@/actions/user-settings', () => ({ updateUserSettingsAction: vi.fn().mockResolvedValue({ success: true, settings: {} }), })) vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } })) -// Mock dnd-kit +// Mock dnd-kit (still needed for PBI panel which supports drag-and-drop) vi.mock('@dnd-kit/core', () => ({ DndContext: ({ children }: { children: React.ReactNode }) => <>{children}, PointerSensor: class {}, diff --git a/__tests__/components/backlog/task-panel.test.tsx b/__tests__/components/backlog/task-panel.test.tsx index 4067c6d..fc5cf7a 100644 --- a/__tests__/components/backlog/task-panel.test.tsx +++ b/__tests__/components/backlog/task-panel.test.tsx @@ -33,37 +33,8 @@ function setActiveStoryAndTasks(storyId: string | null, tasks: BacklogTask[] = [ const mockPush = vi.fn() vi.mock('next/navigation', () => ({ useRouter: () => ({ push: mockPush }) })) -// Mock reorderTasksAction -vi.mock('@/actions/tasks', () => ({ reorderTasksAction: vi.fn().mockResolvedValue({ success: true }) })) vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } })) -// Mock dnd-kit to avoid jsdom drag complexity -vi.mock('@dnd-kit/core', () => ({ - DndContext: ({ children }: { children: React.ReactNode }) => <>{children}, - PointerSensor: class {}, - KeyboardSensor: class {}, - useSensor: vi.fn(), - useSensors: vi.fn(() => []), - closestCenter: vi.fn(), - DragOverlay: () => null, -})) -vi.mock('@dnd-kit/sortable', () => ({ - SortableContext: ({ children }: { children: React.ReactNode }) => <>{children}, - useSortable: () => ({ - attributes: {}, listeners: {}, setNodeRef: vi.fn(), - transform: null, transition: undefined, isDragging: false, - }), - rectSortingStrategy: {}, - sortableKeyboardCoordinates: {}, - arrayMove: (arr: unknown[], from: number, to: number) => { - const next = [...arr] - next.splice(from, 1) - next.splice(to, 0, arr[from]) - return next - }, -})) -vi.mock('@dnd-kit/utilities', () => ({ CSS: { Transform: { toString: () => '' } } })) - import { TaskPanel } from '@/components/backlog/task-panel' const PRODUCT_ID = 'prod-1' @@ -141,12 +112,4 @@ describe('TaskPanel', () => { expect((btn as HTMLButtonElement).disabled).toBe(true) }) - it('cards have no drag listeners in demo mode (whole-card drag disabled)', () => { - setActiveStoryAndTasks(STORY_ID, TASKS) - // In demo mode, listeners ({} from useSortable mock) are not spread onto the card. - // The mock always returns empty listeners, so we just verify the cards render without error. - renderPanel(true) - expect(screen.getByText('Eerste taak')).toBeTruthy() - expect(screen.getByText('Tweede taak')).toBeTruthy() - }) }) diff --git a/docs/INDEX.md b/docs/INDEX.md index 932620b..609d9ba 100644 --- a/docs/INDEX.md +++ b/docs/INDEX.md @@ -18,6 +18,7 @@ Auto-generated on 2026-05-14 from front-matter and headings. | 0007 | [ADR-0007: Agent ↔ user question channel via persistent table + LISTEN/NOTIFY](./adr/0007-claude-question-channel-design.md) | accepted | | 0008 | [ADR-0008: Agent instructions in CLAUDE.md + topical runbooks](./adr/0008-agent-instructions-in-claude-md.md) | accepted | | 0010 | [ADR-0010: Eén product = één repo; cross-product planning vereist (later) een Initiative-laag](./adr/0010-product-per-repo-cross-product-planning.md) | accepted | +| 0011 | [ADR-0011: code is de bindende volgordesleutel voor stories en taken; priority is label](./adr/0011-code-volgordesleutel-stories-taken.md) | accepted | ## Specifications @@ -73,7 +74,7 @@ Auto-generated on 2026-05-14 from front-matter and headings. | [Realtime NOTIFY payload — veldnaam-contract](./patterns/realtime-notify-payload.md) | active | 2026-05-03 | | [Route Handler (REST API)](./patterns/route-handler.md) | active | 2026-05-08 | | [Server Action](./patterns/server-action.md) | active | 2026-05-08 | -| [Float sort_order (drag-and-drop volgorde)](./patterns/sort-order.md) | active | 2026-05-03 | +| [sort_order — PBI drag-and-drop vs. code-bindende volgorde voor stories/taken](./patterns/sort-order.md) | active | 2026-05-14 | | [Story met UI-component](./patterns/story-with-ui-component.md) | active | 2026-05-03 | | [Web Push](./patterns/web-push.md) | active | 2026-05-07 | | [Workspace-store + realtime — bounded-context patroon](./patterns/workspace-store.md) | active | 2026-05-10 | diff --git a/docs/adr/0011-code-volgordesleutel-stories-taken.md b/docs/adr/0011-code-volgordesleutel-stories-taken.md new file mode 100644 index 0000000..b3d72b1 --- /dev/null +++ b/docs/adr/0011-code-volgordesleutel-stories-taken.md @@ -0,0 +1,58 @@ +# ADR-0011: code is de bindende volgordesleutel voor stories en taken; priority is label + +## Status + +accepted + +## Context + +Vóór dit besluit werden stories en taken geordend op `[priority ASC, sort_order ASC]`. +Gebruikers konden de volgorde via drag-and-drop aanpassen (float-insertion in `sort_order`). +Dit leidde tot meerdere problemen: + +1. **Onvoorspelbare uitvoervolgorde voor de AI-agent**: een agent die taken aanmaakt via + `create_task` (in call-volgorde) zag die volgorde ongedaan gemaakt zodra een andere + agent of gebruiker de `priority` aanpaste. +2. **Divergentie tussen `code` en `sort_order`**: `code` reflecteerde de aanmaak-volgorde + (`T-1`, `T-2`, …), maar `sort_order` kon na herordening compleet anders zijn. +3. **DnD-reorder voor stories/taken was overbodig**: de enige betekenisvolle volgorde voor + een AI-gedreven sprint is de creatie-volgorde van taken — handmatige herordening voegde + verwarring toe zonder toegevoegde waarde. + +## Beslissing + +- **`code` is de bindende volgordesleutel** voor stories en taken. +- **`sort_order` is een numerieke spiegel van `code`**, berekend via `parseCodeNumber(code)` + uit `lib/code.ts`. Hierbij extraheert `parseCodeNumber` het trailertal van de code-string + (bijv. `"ST-042"` → `42`, `"T-7"` → `7`). +- **`sort_order` wordt automatisch gezet** bij `create` (server berekent de waarde) en bij + code-edit (PATCH met nieuw `code`). Sprint-membership-acties laten `sort_order` ongemoeid. +- **Drag-and-drop herordening van stories en taken is verwijderd.** Enkel PBI-ordering + gebruikt nog float-insertion (zie ADR-0002). +- **`priority` is een puur label** (urgentie-aanduiding voor de gebruiker). Geen enkele + `orderBy` op stories of taken gebruikt nog `priority`. + +## Gevolgen + +### Positief + +- De uitvoervolgorde van taken is deterministisch en gelijk aan de aanroep-volgorde van + `create_task` — agents kunnen dit betrouwbaar afleiden zonder extra queries. +- `code` en `sort_order` zijn altijd in sync — geen divergentie meer na herordening. +- Minder complexiteit: geen reorder-route, geen reorder-server-actions, geen DnD-context + in backlog-story-panel en task-panel. + +### Negatief + +- Gebruikers kunnen de volgorde van stories en taken niet handmatig herordenen via slepen. + Volgorde aanpassen vereist nu het wijzigen van de `code` (of het aanmaken in de gewenste + volgorde). Dit is een bewuste trade-off ten gunste van voorspelbaarheid voor de agent. +- `parseCodeNumber` retourneert `0` voor codes zonder numeriek suffix (bijv. `"CUSTOM-FOO"`). + Zulke codes clusteren bij positie 0 — vermijdbaar door codes met een numeriek suffix te + gebruiken. + +## Zie ook + +- [ADR-0002: float sort_order voor drag-and-drop (PBI-ordering)](./0002-float-sort-order.md) +- [docs/patterns/sort-order.md](../patterns/sort-order.md) — implementatiepatroon +- `lib/code.ts` — `parseCodeNumber`-helper diff --git a/docs/api/rest-contract.md b/docs/api/rest-contract.md index 2ab906e..b4f5871 100644 --- a/docs/api/rest-contract.md +++ b/docs/api/rest-contract.md @@ -169,7 +169,7 @@ Hoogst geprioriteerde open story in de actieve sprint. ### `GET /api/sprints/:id/tasks` -Lijst taken van de sprint, geordend op `(story.sort_order, task.priority, task.sort_order)`. +Lijst taken van de sprint, geordend op `(story.sort_order, task.sort_order)` — code-volgorde, geen priority. **Query params:** `?limit=N` (default 10, max 50) @@ -193,19 +193,6 @@ Lijst taken van de sprint, geordend op `(story.sort_order, task.priority, task.s --- -### `PATCH /api/stories/:id/tasks/reorder` - -Volgorde van taken binnen een story aanpassen. - -**Body:** -```json -{ "task_ids": ["task-id-a", "task-id-b", "task-id-c"] } -``` - -Alle IDs moeten bij de story horen. **Foutcodes:** `422` bij Zod-fouten of als een task_id niet tot de story behoort. - ---- - ### `PATCH /api/tasks/:id` Status of implementation_plan bijwerken. Minstens één van beide is verplicht. @@ -537,7 +524,7 @@ worden. Tot dan retourneert de stub-default in vitest een lege response. |---|---|---|---| | `ensureProductLoaded(productId)` | `GET /api/products/:id/backlog` | **ontbreekt** | T-870 (Story 7) | | `ensurePbiLoaded(pbiId)` | `GET /api/pbis/:id/stories` | **ontbreekt** (en `/api/pbis` route-folder bestaat nog niet) | T-870 (Story 7) | -| `ensureStoryLoaded(storyId)` | `GET /api/stories/:id/tasks` | **ontbreekt** (alleen `tasks/reorder` bestaat) | T-870 (Story 7) | +| `ensureStoryLoaded(storyId)` | `GET /api/stories/:id/tasks` | **ontbreekt** | T-870 (Story 7) | | `ensureTaskLoaded(taskId)` | `GET /api/tasks/:id` | **ontbreekt** (alleen `PATCH` bestaat) | T-870 (Story 7) | Vereisten voor de toe te voegen routes: diff --git a/docs/patterns/sort-order.md b/docs/patterns/sort-order.md index 2aa41b0..2c3ef2c 100644 --- a/docs/patterns/sort-order.md +++ b/docs/patterns/sort-order.md @@ -1,15 +1,22 @@ --- -title: "Float sort_order (drag-and-drop volgorde)" +title: "sort_order — PBI drag-and-drop vs. code-bindende volgorde voor stories/taken" status: active audience: [ai-agent, contributor] language: nl -last_updated: 2026-05-03 -when_to_read: "When implementing drag-and-drop reordering or inserting between items." +last_updated: 2026-05-14 +when_to_read: "When implementing ordering for PBIs (drag-and-drop) or stories/tasks (code-binding)." --- -# Patroon: Float sort_order (drag-and-drop volgorde) +# Patroon: sort_order — PBI vs. Story/Taak -## Berekening bij tussenvoeging +`sort_order` heeft voor PBI's een andere betekenis dan voor stories en taken. + +--- + +## PBI — float-insertion (drag-and-drop) + +PBI's ondersteunen drag-and-drop herordening. `sort_order` is een `Float` die via de +midpoint-formule wordt berekend bij tussenvoeging: ```ts function getSortOrder(before: number | null, after: number | null): number { @@ -20,9 +27,9 @@ function getSortOrder(before: number | null, after: number | null): number { } ``` -## Herindexeer als precisie opraakt +### Herindexeer als precisie opraakt -Trigger wanneer het kleinste verschil tussen twee opeenvolgende items < 0.001 is. +Trigger wanneer het kleinste verschil tussen twee opeenvolgende PBI's < 0.001 is: ```ts async function reindexIfNeeded(items: { id: string; sort_order: number }[]) { @@ -37,12 +44,62 @@ async function reindexIfNeeded(items: { id: string; sort_order: number }[]) { } ``` -## Reorder Server Actions +### Reorder Server Action (PBI-only) -Een drag-and-drop reorder stuurt altijd client-controlled ID-lijsten naar de server. Behandel die lijst als onbetrouwbaar. +Een drag-and-drop reorder stuurt client-controlled ID-lijsten naar de server. +Behandel die lijst als onbetrouwbaar: - Weiger dubbele IDs. -- Haal alle IDs op met de parent-scope, bijvoorbeeld `product_id`, `pbi_id`, `sprint_id` of `story_id`. +- Haal alle IDs op met de parent-scope (`product_id`). - Weiger de operatie als het aantal gevonden records niet exact gelijk is aan het aantal aangeleverde IDs. - Update pas daarna `sort_order` in een transactie. - Gebruik bij priority changes dezelfde parent uit de database, niet een los meegegeven `productId`. + +--- + +## Story / Taak — code-bindende volgorde (geen drag-and-drop) + +Voor stories en taken is `sort_order` een **numerieke spiegel van `code`**, berekend via +`parseCodeNumber(code)` uit `lib/code.ts`. Drag-and-drop herordening bestaat niet voor +stories en taken. + +### Wanneer `sort_order` wordt gezet + +| Moment | Wat er gebeurt | +|---|---| +| `story.create` / `task.create` | `sort_order = parseCodeNumber(code)` | +| Idea-materialisatie (`materializeIdeaPlanAction`) | idem — stories en taken krijgen `sort_order = parseCodeNumber(storyCode / taskCode)` | +| Code-edit (PATCH met nieuw `code`) | `sort_order = parseCodeNumber(newCode)` wordt bijgewerkt | +| Sprint-membership-acties | `sort_order` wordt **niet** aangeraakt | + +### `parseCodeNumber` + +Extraheert het trailertal uit een code-string: + +```ts +// lib/code.ts +export function parseCodeNumber(code: string): number { + const match = code.match(/(\d+)$/) + return match ? parseInt(match[1], 10) : 0 +} +``` + +Voorbeelden: `"ST-042"` → `42`, `"T-7"` → `7`, `"CUSTOM-FOO"` → `0`. + +### Ordering queries + +Stories en taken worden **uitsluitend** op `sort_order` geordend — nooit op `priority`: + +```ts +// stories binnen een sprint +orderBy: [{ sort_order: 'asc' }] + +// taken binnen een story +orderBy: { sort_order: 'asc' } + +// taken binnen een sprint (story-volgorde eerst) +orderBy: [{ story: { sort_order: 'asc' } }, { sort_order: 'asc' }] +``` + +`priority` is een **label** (urgentie-aanduiding voor de gebruiker), geen +sorteerkriteria voor stories of taken. diff --git a/docs/runbooks/plan-to-pbi-flow.md b/docs/runbooks/plan-to-pbi-flow.md index 0d8356d..beac63a 100644 --- a/docs/runbooks/plan-to-pbi-flow.md +++ b/docs/runbooks/plan-to-pbi-flow.md @@ -80,14 +80,15 @@ plan goedgekeurd ### 2. `create_story` ``` -{ pbi_id, title, description?, acceptance_criteria?, priority, sort_order? } +{ pbi_id, sprint_id, title, description?, acceptance_criteria?, priority } ``` - **`title`** — concreet, in user-story stijl als dat past ("Als developer wil ik …") - **`description`** — technische context, scope-grenzen, niet-doelen - **`acceptance_criteria`** — markdown checklist (`- [ ] …`); bepaalt wanneer de Story `DONE` is - `product_id` wordt afgeleid uit de PBI — niet meegeven -- **Sprint-koppeling:** de story wordt aan de actieve OPEN-sprint gehangen (de zojuist aangemaakte). Als er meerdere OPEN-sprints bestaan: bevestig eerst met de gebruiker welke sprint geldt, of breidt `create_story` uit met een expliciete `sprint_id` parameter (apart PBI). +- **`sprint_id`** — geef de zojuist aangemaakte sprint mee. Als er meerdere OPEN-sprints bestaan: bevestig eerst met de gebruiker welke sprint geldt. +- **`sort_order`** — NIET meegeven. De server berekent `sort_order = parseCodeNumber(code)` automatisch. De volgorde van stories = de volgorde van hun codes (= aanroep-volgorde); niet priority. - Status start op `OPEN` > **Eén story per PBI** is de gebruikelijke verhouding. Splits alleen op in meerdere stories als het plan logisch in onafhankelijk-shipbare delen valt — let op dat dit dan ook meerdere sprints betekent. @@ -95,17 +96,17 @@ plan goedgekeurd ### 3. `create_task` (één call per taak) ``` -{ story_id, title, description?, implementation_plan?, priority, sort_order? } +{ story_id, title, description?, implementation_plan?, priority } ``` - **`title`** — werkwoord-vorm: "Implementeer …", "Verplaats …", "Voeg test toe voor …" - **`description`** — wat de taak afdekt, in 1-3 zinnen - **`implementation_plan`** — **belangrijk**: markdown met de daadwerkelijke stappen + file-paths + reuse-pointers; dit is wat de Implementation-agent later inleest -- **`sort_order`** — leeg laten voor de eerste call; daarna server-side `last + 1`, of expliciet meegeven om volgorde af te dwingen +- **`sort_order`** — NIET meegeven. De server berekent `sort_order = parseCodeNumber(code)` automatisch. De uitvoervolgorde = de aanroep-volgorde (= code-volgorde); niet priority. - `sprint_id` wordt geërfd van de Story — niet meegeven - Status start op `TO_DO` -> De gebruiker werkt taken af in **sort_order**. Zet voorbereidende taken (data-model, types) vóór UI-taken; tests komen ná de feature-implementatie tenzij TDD expliciet is afgesproken. +> De uitvoervolgorde van taken is gelijk aan de **aanroep-volgorde** van `create_task` (= code-volgorde). `priority` is een label (urgentie), géén sorteerkriteria. Zet voorbereidende taken (data-model, types) vóór UI-taken; tests komen ná de feature-implementatie tenzij TDD expliciet is afgesproken. --- diff --git a/lib/idea-prompts/make-plan.md b/lib/idea-prompts/make-plan.md index d4852cb..a53b8c9 100644 --- a/lib/idea-prompts/make-plan.md +++ b/lib/idea-prompts/make-plan.md @@ -102,7 +102,7 @@ stories: 2. Test toevoegen Y 3. Verifieer Z # task.priority is optioneel en wordt door materialize GENEGEERD. - # Tasks erven story.priority; sort_order = positie in deze tasks-array. + # Tasks erven story.priority; sort_order wordt afgeleid van de auto-code. verify_required: ALIGNED_OR_PARTIAL # ALIGNED | ALIGNED_OR_PARTIAL | ANY verify_only: false # true voor pure verify-passes - title: "Taak B" @@ -146,9 +146,9 @@ Beschrijf: - `pbi.priority`, `story.priority`: integer 1–4, **verplicht**. - `task.priority`: integer 1–4, **optioneel**. **Wordt door materialize genegeerd** ten faveure van story-priority — alle tasks binnen een story erven dezelfde - priority. Reden: worker sorteert op `priority ASC, sort_order ASC`; gemixte - task-priorities zouden de YAML-volgorde verstoren. De YAML-volgorde *is* de - execution-volgorde — daar is `sort_order` (positie in de array) voor. + priority. `priority` is een **label** (urgentie), géén sorteerkriteria voor stories + of taken. De **YAML-array-volgorde** is de execution-volgorde: de server berekent + `sort_order = parseCodeNumber(auto-code)` op basis van aanroep-volgorde. - Minimaal 1 story; per story minimaal 1 taak. - `implementation_plan`: max 8000 chars. - `verify_required`: enum exact `ALIGNED` | `ALIGNED_OR_PARTIAL` | `ANY`.