feat(PBI-115): cross-sprint eligibility + verplaats-knop in draft #59

Merged
janpeter merged 5 commits from feat/sprint-draft-eligibility into main 2026-05-30 10:06:24 +02:00
Owner

Summary

Fix voor de "Geen eligible stories voor deze sprint"-bug in de draft-flow: sprint-aanmaak respecteert nu cross-sprint eligibility, toont welke sprint blokkeert, en biedt een "Verplaats stories hierheen"-knop.

De PBI-79 cross-sprint-infrastructuur (sprint-membership-summary + cross-sprint-blocks endpoints, store-fetchers, setters) was wel gebouwd (T-928/T-929) maar nooit in productie aangesloten — deze PR sluit die aan op de draft-flow.

Server (actions/sprints.ts, T-1253)

  • createSprintWithSelectionAction accepteert nu een move: string[] parameter; alleen IDs die in partition.crossSprint zitten worden herkoppeld aan de nieuwe sprint (DONE-stories zijn nooit verplaatsbaar).
  • Bij 0-eligible retourneert de actie nu conflicts.crossSprint i.p.v. een kale fout, zodat de UI kan tonen welke sprint de stories vasthoudt.
  • Verplaatsen mag ook als de oude sprint BUILDING is (die verliest de stories).

Wiring (components/backlog/sprint-draft-banner.tsx, T-1254)

  • Op draft-mount/dep-change: fetchCrossSprintBlocks(productId, null, pbiIds) vult crossSprintBlocks voor de gekozen PBI's.
  • Op unmount: setCrossSprintBlocks({}) voorkomt stale data in state B.

UI (components/backlog/sprint-definition-banner.tsx + pbi-list.tsx, T-1255)

  • computeCounts telt geblokkeerde stories niet als geselecteerd; sub-regel toont "X stories zitten al in sprint Y" (of "in N andere sprints").
  • Nieuwe outline-knop "Verplaats stories hierheen" roept de server-action aan met move=[blockedStoryIds].
  • PBI-tri-state toont partial als (een deel van) child-stories geblokkeerd is.

Tests (T-1256)

  • 3 nieuwe action-cases: move-pad, DONE-not-moved, 0-eligible-conflicts.
  • 5 computeCounts cases (computeCounts geëxtraheerd naar sprint-definition-banner.utils.ts voor pure-function isolatie).
  • 3 draft-banner wiring cases (fetch + unmount).

Test plan

  • Backlog → "Nieuwe sprint" → selecteer een PBI waarvan de stories in een andere OPEN sprint zitten:
    • Banner sub-regel: "X stories zitten al in sprint Y"
    • PBI-vinkje toont partial (—) i.p.v. full (✓)
    • Teller telt geblokkeerde stories niet als geselecteerd
    • "Sprint aanmaken" alleen → toast "Alle stories zitten al in sprint Y — gebruik 'Verplaats stories hierheen'"
  • "Verplaats stories hierheen" → nieuwe sprint aangemaakt; oude sprint verliest die stories; tasks erven nieuwe sprint_id
  • DONE-story in de PBI → niet verplaatst (komt in conflicts.notEligible)
  • Mix van eligible + cross-sprint + DONE: alleen eligible + expliciet-gemovede gaan mee
  • Draft-cancel → crossSprintBlocks leeg in store (geen stale data voor state B)
  • npm run verify: nieuwe + bestaande tests groen (lokaal 1259/1259 passing)

Notes

  • Sprint S-2026-05-29-eligibility · Story ST-1440 · Tasks T-1253 t/m T-1256 · Plan in docs/plans/sprint-draft-cross-sprint-eligibility.md
  • Niet in scope: state B (membership op een al-actieve sprint) en deriveScreenState() — aparte follow-up.
  • CI typecheck zal rood zijn door 3 pre-existing ideaNotification-errors op main (actions/ideas.ts, lib/idea-ready-cards.ts); niet uit deze PR, separate fix nodig.
## Summary Fix voor de "Geen eligible stories voor deze sprint"-bug in de draft-flow: sprint-aanmaak respecteert nu cross-sprint eligibility, toont welke sprint blokkeert, en biedt een **"Verplaats stories hierheen"**-knop. De PBI-79 cross-sprint-infrastructuur (`sprint-membership-summary` + `cross-sprint-blocks` endpoints, store-fetchers, setters) was wel gebouwd (T-928/T-929) maar nooit in productie aangesloten — deze PR sluit die aan op de draft-flow. ### Server (`actions/sprints.ts`, T-1253) - `createSprintWithSelectionAction` accepteert nu een `move: string[]` parameter; alleen IDs die in `partition.crossSprint` zitten worden herkoppeld aan de nieuwe sprint (DONE-stories zijn nooit verplaatsbaar). - Bij 0-eligible retourneert de actie nu `conflicts.crossSprint` i.p.v. een kale fout, zodat de UI kan tonen welke sprint de stories vasthoudt. - Verplaatsen mag ook als de oude sprint `BUILDING` is (die verliest de stories). ### Wiring (`components/backlog/sprint-draft-banner.tsx`, T-1254) - Op draft-mount/dep-change: `fetchCrossSprintBlocks(productId, null, pbiIds)` vult `crossSprintBlocks` voor de gekozen PBI's. - Op unmount: `setCrossSprintBlocks({})` voorkomt stale data in state B. ### UI (`components/backlog/sprint-definition-banner.tsx` + `pbi-list.tsx`, T-1255) - `computeCounts` telt geblokkeerde stories niet als geselecteerd; sub-regel toont *"X stories zitten al in sprint Y"* (of *"in N andere sprints"*). - Nieuwe outline-knop **"Verplaats stories hierheen"** roept de server-action aan met `move=[blockedStoryIds]`. - PBI-tri-state toont `partial` als (een deel van) child-stories geblokkeerd is. ### Tests (T-1256) - 3 nieuwe action-cases: move-pad, DONE-not-moved, 0-eligible-conflicts. - 5 `computeCounts` cases (`computeCounts` geëxtraheerd naar `sprint-definition-banner.utils.ts` voor pure-function isolatie). - 3 draft-banner wiring cases (fetch + unmount). ## Test plan - [ ] Backlog → "Nieuwe sprint" → selecteer een PBI waarvan de stories in een andere OPEN sprint zitten: - [ ] Banner sub-regel: *"X stories zitten al in sprint Y"* - [ ] PBI-vinkje toont `partial` (—) i.p.v. `full` (✓) - [ ] Teller telt geblokkeerde stories niet als geselecteerd - [ ] "Sprint aanmaken" alleen → toast "Alle stories zitten al in sprint Y — gebruik 'Verplaats stories hierheen'" - [ ] "Verplaats stories hierheen" → nieuwe sprint aangemaakt; oude sprint verliest die stories; tasks erven nieuwe `sprint_id` - [ ] DONE-story in de PBI → niet verplaatst (komt in `conflicts.notEligible`) - [ ] Mix van eligible + cross-sprint + DONE: alleen eligible + expliciet-gemovede gaan mee - [ ] Draft-cancel → `crossSprintBlocks` leeg in store (geen stale data voor state B) - [ ] `npm run verify`: nieuwe + bestaande tests groen (lokaal 1259/1259 passing) ## Notes - Sprint **S-2026-05-29-eligibility** · Story **ST-1440** · Tasks **T-1253 t/m T-1256** · Plan in [`docs/plans/sprint-draft-cross-sprint-eligibility.md`](docs/plans/sprint-draft-cross-sprint-eligibility.md) - **Niet in scope**: state B (membership op een al-actieve sprint) en `deriveScreenState()` — aparte follow-up. - **CI typecheck zal rood zijn** door 3 pre-existing `ideaNotification`-errors op `main` (`actions/ideas.ts`, `lib/idea-ready-cards.ts`); niet uit deze PR, separate fix nodig.
PBI-115 / ST-1440. Plan-doc gekoppeld als PLAN-revisie aan de PBI via
create_product_doc (MCP). Beschrijft de wortels van de "Geen eligible
stories voor deze sprint"-bug, de niet-aangesloten PBI-79-infra, en de
gekozen aanpak (melding + verplaats-knop).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Schema-uitbreiding: `move: string[]` met default [].
- Bij 0-final-stories retourneert de actie nu conflicts.crossSprint zodat
  de UI kan tonen wélke sprint blokkeert (i.p.v. een kale fout).
- Stories in `move` ∩ partition.crossSprint worden in de transactie
  herkoppeld aan de nieuwe sprint (sprint_id overwrite). DONE-stories
  zitten nooit in crossSprint, dus kunnen niet verplaatst worden.
- conflicts.notEligible/crossSprint in de success-return zijn nu zonder
  verplaatste IDs.
- Bestaande 6 tests blijven groen (default move=[] = ongewijzigd gedrag).

Reuse: partitionByEligibility (lib/sprint-conflicts.ts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SprintDraftBanner haalt nu bij iedere draft-PBI-wijziging de cross-sprint-
blokkades op via useProductWorkspaceStore.fetchCrossSprintBlocks (geen
excludeSprintId — nieuwe sprint bestaat nog niet). Bij draft-cancel
unmount het component en wist een tweede effect crossSprintBlocks zodat
state-B-consumers geen stale data zien.

Sluit de PBI-79-infra (T-928/T-929) aan die wel gebouwd maar nooit
aangeroepen werd. UI in T-1255 leest hier crossSprintBlocks uit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
components/backlog/sprint-definition-banner.tsx:
- computeCounts gebruikt nu relations.storyIdsByPbi + crossSprintBlocks
  (workspace-store) i.p.v. pbiSummary — die laatste wordt in de draft-
  flow toch niet gevuld. Geblokkeerde stories tellen niet als geselecteerd
  en verzamelen sprintNames + storyIds voor de banner-sub-regel + move-knop.
- Sub-regel "X stor(y/ies) zitten al in sprint Y" (of "in N andere sprints").
- Nieuwe knop "Verplaats stories hierheen" → createSprintWithSelectionAction
  met move=[geblokkeerde ids]. Bij error met conflicts.crossSprint toont
  de toast nu welke sprint blokkeert i.p.v. de kale fout.
- computeCounts geëxporteerd voor T-1256 (banner-tellertest).

components/backlog/pbi-list.tsx:
- SortablePbiRowWithTriState: een vol-gemarkeerde PBI toont 'partial' als
  (een deel van) zijn child-stories in crossSprintBlocks zit. Geldt voor
  zowel state A' (draft) als state B (membership) — de blocks worden
  vandaag alleen in draft-context gevuld (T-1254).

actions/sprints.ts:
- CreateSprintWithSelectionInput gebruikt nu z.input i.p.v. z.infer zodat
  velden met een Zod-default (pbiIntent, storyOverrides, move) optioneel
  zijn voor callers. Onveranderd runtime-gedrag.

components/backlog/sprint-draft-banner.tsx:
- fetchCrossSprintBlocks(productId, null, pbiIds): excludeSprintId is
  string|null, niet undefined. Pure type-fix.

Tests: 31/31 (create-sprint-with-selection + sprint-membership). Typecheck:
mijn errors weg; resterende 7 errors zijn pre-existing (manual.generated,
ideaNotification, idea-ready-cards) en niet van deze taak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
test(ST-1440): tests voor move-pad, 0-eligible-conflicts, banner-teller, draft-fetch (T-1256)
All checks were successful
CI / Lint, Typecheck, Test & Build (pull_request) Successful in 4m1s
CI / Deploy Manual (workflow_dispatch) (pull_request) Has been skipped
CI / Detect deploy-relevant changes (pull_request) Has been skipped
CI / Deploy Preview (PR) (pull_request) Has been skipped
CI / Deploy Production (main) (pull_request) Has been skipped
689bba0f89
__tests__/actions/create-sprint-with-selection.test.ts — 3 nieuwe cases:
- move=[id] herkoppelt een cross-sprint story aan de nieuwe sprint
- DONE-story in move-lijst wordt NIET verplaatst (zit in notEligible,
  niet in crossSprint)
- 0-eligible retourneert conflicts.crossSprint (regressie op de nieuwe
  return-shape uit T-1253)

__tests__/components/backlog/sprint-definition-banner.test.ts — 5 cases
voor computeCounts: telt zonder blokkades, trekt geblokkeerde stories af +
verzamelt sprintNames, respecteert overrides.remove/.add, meerdere
blokkerende sprints, en geblokkeerde overrides.add.

__tests__/components/backlog/sprint-draft-banner.test.tsx — 3 cases voor
de wiring: geen draft → niets gefetcht, draft → fetchCrossSprintBlocks
met de juiste pbiIds (intent=all + overrides), unmount → setCrossSprintBlocks({}).

components/backlog/sprint-definition-banner.utils.ts (nieuw) +
components/backlog/sprint-definition-banner.tsx — computeCounts +
formatBlockingSprints geëxtraheerd naar een puur utils-bestand zodat de
test ze in isolatie kan importeren zonder de hele banner-import-chain
(prisma, stores, actions) op te tuigen.

Full suite: 144 files / 1259 tests passing (~12.7s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
janpeter/Scrum4Me!59
No description provided.