Files
atomic-design-poc/docs/project/backlog
ehoandClaude Sonnet 5 5d73ca21f6 feat(backend): enforce the scholing threshold server-side (WP-69)
ADR-0001's own canonical "config value" example was unenforced: GET
/intake/policy echoed ScholingThreshold, but no request DTO carried a
scholing answer, so the server had nothing to re-validate. A crafted
POST could skip a requirement the wizard presents as mandatory.

IntakePolicy.RejectIncompleteScholing is the authority — three-valued
completeness (below threshold an answer is required; "nee" is legal and
still submits; punten only belong to a followed scholing), living in the
class that owns the constant so scripts/check-seam.sh keeps guarding the
FE/BE literal pair. Both submit paths call it; a violation 400s with
ProblemDetails and leaves the aanvraag a Concept. Gated on
Type == "intake" (the endpoint's switch lumps herregistratie with
intake, which has no scholing question), and guarded by `reject is null`
so a zero-uren submission is still decided on its merits.

Also fixes a live FE bug in the same rule: validateStep required punten
whenever scholingGevolgd was 'ja' regardless of lageUren, while the
template renders those fields only when lageUren — so answering 'ja'
then raising uren either blocked the user on an invisible field or
emitted aanvullendeScholing: undefined alongside punten. punten now
derives from aanvullendeScholing, so that combination is unrepresentable
in ValidIntake.

Note: EndpointTests' Worked_hours_submission_succeeds was itself
asserting the vulnerable payload ({ uren: 40 }, no answer) and needed a
complete answer added; the zero-hours rows are the ordering regression
net and are unmodified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 22:42:14 +02:00
..

Backlog — showcase hardening

Ordered work packages that take this POC from "good" to reference showcase: CIBG design-system fidelity, DDD/FP consistency, Storybook as curriculum, and WCAG compliance with automated gates. Source: the architecture/CIBG/a11y audit of 2026-07-02 (plan: "Showcase hardening").

This backlog supersedes docs/project/SHOWCASE-ROADMAP.md.

Session protocol

  • One WP per session. Read CLAUDE.md, this README, the WP file, and the WP's "Read first" list — then execute. Do not start the next WP in the same session.
  • Match the model to the step, not the whole session (see CLAUDE.md's "Model routing for agent delegation"). Read the WP's Decisions block with the planner agent (Opus) if the current session isn't already Opus — the approach and edge cases deserve the stronger model. Implement directly if already on Sonnet, or hand off to the developer agent otherwise, once the plan is approved. Delegate simple, read-only CLI checks (running a test suite, grep/git status, verifying a file exists) to the task-runner agent (Haiku) — proportion cost to the step's difficulty, not the WP's.
  • The Decisions block in each WP is pre-made — don't relitigate it.
  • A WP ends GREEN (below) with its acceptance criteria checked off and its Status updated to done (+ commit hash).
  • No WP leaves a lint rule/check disabled without an inline justification comment and a cross-reference to the WP that will remove it.

GREEN (global definition of done)

npm run lint && npm run check:tokens && npm test && npm run build && npm run build-storybook

From WP-01 onward, additionally:

npm run test-storybook:ci

Phases 05 were frontend-only; phase 6 (Brief v2) touches backend/ — for those WPs cd backend && dotnet test is part of GREEN, and any wire change ends with npm run gen:api leaving no drift.

From WP-19 onward, npm run e2e is part of CI (its own job) but NOT part of the local GREEN one-liner above — it needs the real backend + npm start already running (see WP-19's own file), so it's a separate manual/CI step, not chained into the others.

Order

Gates land before the work they cover; each lint rule lands in the same WP as the fixes for its existing violations, so every WP ends green.

WP Title Phase Status
WP-01 Axe-on-every-story CI gate 0 · gates done
WP-02 Harden check:tokens + fix what it catches 0 · gates done
WP-03 Boundaries I: contracts purity + ApiClient confinement 0 · gates done
WP-04 Boundaries II: ui ↛ infrastructure + showcase sanction 0 · gates done
WP-05 Parse-don't-validate closure + MDX 1 · FP/DDD done
WP-06 Generic async template contexts — kill $any() 1 · FP/DDD done
WP-07 Brief on the shared idioms + RemoteData MDX 1 · FP/DDD done
WP-08 One store idiom + machine naming + TEA MDX 1 · FP/DDD done
WP-09 Pure-logic closure: dates + missing command specs 1 · FP/DDD done
WP-10 CIBG button fidelity 2 · CIBG done
WP-11 CIBG markup fidelity: application-link + absent-class triage 2 · CIBG done
WP-12 CIBG Datablock for application data 2 · CIBG done
WP-13 CIBG-gap register + hygiene + MDX 2 · CIBG done
WP-14 Storybook taxonomy reorg + Layers MDX 3 · Storybook done
WP-15 Missing stories: shell + brief components 3 · Storybook done
WP-16 Component a11y: description wiring + alert role 4 · a11y done
WP-17 App-level a11y: route focus, template lint, WCAG checklist 4 · a11y done
WP-18 ABAC capability spine (Principal + capabilities, phase P1) 5 · productie-volwassenheid done
WP-19 Playwright e2e smoke 5 · productie-volwassenheid done
WP-20 Second locale proof 5 · productie-volwassenheid done
WP-21 Resilience seams (correlation-id, idempotency, retry) 5 · productie-volwassenheid done
WP-22 Durable persistence (optional tier) 5 · productie-volwassenheid done
WP-23 Org-template backend + admin role 6 · Brief v2 done
WP-24 Letter canvas (edit on the letter) 6 · Brief v2 done
WP-25 Server-rendered letter preview (HTML; PDF deferred) 6 · Brief v2 done
WP-26 Admin org-template editor 6 · Brief v2 done
WP-27 Brief UX layer (undo/redo, standaardbrief, diff) 6 · Brief v2 done
WP-28 Brief v2 demo polish (scenarios, e2e, docs) 6 · Brief v2 done
WP-29 Stamdata beheer editor (low-code, PR-emitting) follow-on · ADR-0004 done
WP-30 CI performance follow-ups (node_modules cache, runner image, path filters) follow-on · CI/infra done
WP-31 Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) 7 · refinements done
WP-32 Undo/redo in the stamdata editor (folded into WP-31 — no separate file) 7 · refinements done
WP-33 In-app dev switchers (scenario + role) 7 · refinements done
WP-34 Adres: phone field + BRP address read-only 7 · refinements done
WP-35 One Concept per case type (server-enforced) 7 · refinements done
WP-36 Admin cases page + admin delete 7 · refinements done
WP-37 Dev-switcher reset fix (scenario/role URL param) 8 · platform/DX/showcase done
WP-38 Dependency graph + declarative boundaries (visualize + enforce) 8 · platform/DX/showcase done
WP-39 Showcase: linked code snippets + teaching animations 8 · platform/DX/showcase done
WP-40 PII kernel: branded Bsn VO (elfproef) + masked-value atom 8 · platform/DX/showcase done
WP-41 Persisted, queryable authz/PII-reveal audit (no PII) 8 · platform/DX/showcase done
WP-42 Privacy & security showcase page (mask + no-PII log) 8 · platform/DX/showcase done
WP-43 Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) 8 · platform/DX/showcase done
WP-44 Runnable generator: gen:context 8 · platform/DX/showcase done
WP-45 create-frontend bootstrap generator (mechanise new-ssp) 8 · platform/DX/showcase done
WP-46 Vitest coverage (report + report-only thresholds) 8 · platform/DX/showcase done
WP-47 Runtime feature flags (catalog-in-code, admin toggle, FE+backend) 8 · platform/DX/showcase done
WP-48 Stamdata deletion protection (CI referential gate + editor expire/warn) 8 · platform/DX/showcase done
WP-49 OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) 9 · OpenZaak/ZGW done
WP-50 OpenZaak create-zaak (first write slice) 9 · OpenZaak/ZGW done
WP-51 OpenZaak Documenten (DRC) upload + zaak link 9 · OpenZaak/ZGW done
WP-52 OpenZaak Notificaties (NRC) live status via webhook 9 · OpenZaak/ZGW done
WP-53 Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) 9 · OpenZaak/ZGW done
WP-54 Docker OpenZaak integration-test harness (opt-in, live round-trip) 9 · OpenZaak/ZGW done
WP-55 Real secrets + TLS for the OpenZaak harness 10 · OpenZaak hardening done
WP-56 Idempotent catalogus provisioning 10 · OpenZaak hardening done
WP-57 Least-privilege client scopes 10 · OpenZaak hardening done
WP-58 Real notifications (celery + scripted abonnement) 10 · OpenZaak hardening done
WP-59 Per-document-type confidentialiteit config 10 · OpenZaak hardening done
WP-60 Write-divergence resilience (local + ZGW writes) 10 · OpenZaak hardening done
WP-61 Bootstrap the behandelportal app 11 · Behandelportal done
WP-62 Backend: medewerker caller identity + authz seam 11 · Behandelportal done
WP-63 Backend: aanvraag status lifecycle as a published DTO 11 · Behandelportal done
WP-64 Behandelportal: werkvoorraad (queue) screen 11 · Behandelportal done
WP-65 Behandelportal: zaak detail + beoordeling (decision) screen 11 · Behandelportal done
WP-66 Wire the decision into OpenZaak 11 · Behandelportal done
WP-67 Merge behandelportal into this repo as a monorepo 11 · Behandelportal done
WP-68 Aggregate invariants + status modelling (architecture review) 12 · DDD hardening done
WP-69 Enforce the scholing threshold server-side 12 · DDD hardening todo
WP-70 Test-data builders: illegal fixtures unrepresentable (ADR-0006) 12 · DDD hardening done
WP-71 Test framework coherence: BDD/DDD alignment, close the escape hatches 12 · DDD hardening done

Sequencing dependencies (stated in the WPs too): 01 before 1015 (axe covers story churn); 03/04 before 0509 (boundaries stop new violations during refactors); 06 before 07 (typed <app-async> before brief adopts it); 13 defines the gap-marker format that 11/12 reference — if 11/12 run first, they define it and 13 adopts it. 1822 (phase 5, "productie-volwassenheid") are independent of each other and of phases 14 — pick any order; 18 is the recommended first pick (it's the headline gap: no authorization spine exists yet, and it closes the FE-computed-authz anti-pattern in brief.store.ts). 22 is explicitly lower priority — the current in-memory persistence is a documented, defensible POC choice, not a bug. Phase 6 (Brief v2, the "Brief opstellen v2" PRD) is strictly ordered 23 → 24 → 25 → 26 → 27 → 28: 24 needs 23's orgTemplate on the wire, 25 needs 24's letter.css contract, 26 needs 23's endpoints + 24's canvas, 27/28 polish on top. Phase 7 (refinements, WP-31..36): only 31 → 32 is ordered (32 reuses the createHistory helper 31 extracts); 3336 are independent — pick any order. Phase 8 (platform/DX/showcase, WP-37..46): goal is BOTH a teaching showcase AND a production starter template. Priority: WP-37 (P0 quick fix) → WP-46 + WP-38 + WP-39 (P1, parallel) → WP-40 (P2) → WP-43 (P3) → WP-41 → WP-42 → WP-44 → WP-45 (P4). Ordering constraints: 41 needs 40's PII kernel; 42 needs 40+41; 44 (gen:context) needs 38 (declarative boundaries make the generator simple) + 43; 45 (create-frontend) needs 43+44. 37/38/39/40/43/46 are otherwise independent. Two open tool forks, decided as step 1 of their WP: 38 dependency-cruiser vs Sheriff; 43 plop vs Angular schematics. Phase 9 (OpenZaak/ZGW integration, WP-49..52) is strictly ordered 49 → 50 → 51 → 52: 49 lands the source seam + ZGW client the rest reuse; 50 (create) needs a zaak to exist; 51 (documents) links to 50's zaak; 52 (notificaties) reacts to changes on the zaken 4951 manage. All slices stay behind Zgw:Enabled (default off) so the POC keeps running offline. Two cross-cutting WPs sit alongside that read→write arc: 53 (a real per-request identity seam + citizen-scoping — replaces the stubbed owner/BSN; production-blocking for a real deployment of 4952) and 54 (a docker OpenZaak harness + opt-in integration test — makes 50/51/52 developable against a live instance instead of only fixtures). 53 is independent of the CRUD arc and can land any time; 54 depends on 49 (something to read) and unlocks realistic testing for the rest. Both are self-contained (each WP file carries its own current-state handoff) and sized for a fresh Sonnet session. Phase 10 (OpenZaak production hardening, WP-55..60 — now done) and Phase 11 (Behandelportal, WP-61..66) are two independent tracks that can be worked concurrently — neither blocks the other. Within phase 10: 55/59/60 were fully independent; 57 and 58 both built on 56's provisioning mechanism, otherwise independent of each other. Within phase 11: 61 (bootstrap), 62 (backend medewerker identity), and 63 (backend status DTO) are independent of each other and can land in any order; 64 needs all three (61 for the app to exist, 62 for identity, 63 for the status it reads); 65 needs 64; 66 needs 65 and — now that WP-60 has landed (bounded retry + flagged divergence in ZgwHttpClient/Program.cs) — inherits that retry for free, but must call RecordZgwDivergence on its own besluit write path to get the flagging half too.

WP template

# WP-NN — Title

Status: todo | in-progress | done (<commit>)
Phase: N — name

## Why

## Read first

## Decisions (pre-made, don't relitigate)

## Files

## Steps

## Acceptance criteria

## Verification

## Out of scope

## Risks