# 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) ```bash npm run lint && npm run check:tokens && npm test && npm run build && npm run build-storybook ``` From WP-01 onward, additionally: ```bash npm run test-storybook:ci ``` Phases 0–5 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](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done | | [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done | | [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done | | [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done | | [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done | | [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done | | [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done | | [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done | | [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done | | [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done | | [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done | | [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done | | [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done | | [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done | | [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done | | [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done | | [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done | | [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done | | [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done | | [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done | | [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done | | [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done | | [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done | | [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done | | [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done | | [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done | | [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done | | [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | done | | [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done | | [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | done | | [WP-31](WP-31-shared-store-helpers.md) | 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](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done | | [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done | | [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done | | [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done | | [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done | | [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done | | [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done | | [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done | | [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | done | | [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | done | | [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) | 8 · platform/DX/showcase | done | | [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | done | | [WP-45](WP-45-create-frontend-generator.md) | `create-frontend` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | done | | [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done | | [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done | | [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done | | [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done | | [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | done | | [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done | | [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done | | [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done | | [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done | | [WP-55](WP-55-openzaak-secrets-tls.md) | Real secrets + TLS for the OpenZaak harness | 10 · OpenZaak hardening | done | | [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | done | | [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | done | | [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | done | | [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | done | | [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | done | | [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | done | | [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | done | | [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | done | | [WP-64](WP-64-behandelportal-werkvoorraad.md) | Behandelportal: werkvoorraad (queue) screen | 11 · Behandelportal | done | | [WP-65](WP-65-behandelportal-beoordeling.md) | Behandelportal: zaak detail + beoordeling (decision) screen | 11 · Behandelportal | done | | [WP-66](WP-66-behandelportal-openzaak-write.md) | Wire the decision into OpenZaak | 11 · Behandelportal | done | | [WP-67](WP-67-monorepo-behandelportal.md) | Merge behandelportal into this repo as a monorepo | 11 · Behandelportal | done | | [WP-68](WP-68-ddd-aggregate-hardening.md) | Aggregate invariants + status modelling (architecture review) | 12 · DDD hardening | done | | [WP-69](WP-69-intake-scholing-threshold-enforcement.md) | Enforce the scholing threshold server-side | 12 · DDD hardening | done | | [WP-70](WP-70-test-data-builders.md) | Test-data builders: illegal fixtures unrepresentable (ADR-0006) | 12 · DDD hardening | done | | [WP-71](WP-71-test-framework-coherence.md) | Test framework coherence: BDD/DDD alignment, close the escape hatches | 12 · DDD hardening | done | | [WP-72](WP-72-delete-legacy-submit-endpoints.md) | Delete the dead legacy submit endpoints | 12 · DDD hardening | done | | [WP-73](WP-73-domain-unions.md) | `RegistrationStatus` and `Aanvraag` as closed unions | 12 · DDD hardening | done | | [WP-74](WP-74-e2e-isolation.md) | E2E isolation without a new backend endpoint | 12 · DDD hardening | done | | [WP-75](WP-75-fe-be-seam-closure.md) | Close the remaining FE/BE seams | 12 · DDD hardening | done | Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn); 03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed `` 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. 18–22 (phase 5, "productie-volwassenheid") are independent of each other and of phases 1–4 — 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); 33–36 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 49–51 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 49–52) 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 ```markdown # WP-NN — Title Status: todo | in-progress | done () Phase: N — name ## Why ## Read first ## Decisions (pre-made, don't relitigate) ## Files ## Steps ## Acceptance criteria ## Verification ## Out of scope ## Risks ```