Commit Graph
100 Commits
Author SHA1 Message Date
ehoandClaude Sonnet 5 7b6cabfc4a feat(dx): gen:context generator (WP-44)
npm run gen:context scaffolds a bounded context: folders + starter page, the @<ctx>/*
tsconfig alias, a dependency-cruiser boundary entry, and a lazy authGuard route.

Refactors .dependency-cruiser.js's per-context contextRule calls into a single
CONTEXT_ALLOWED map that every rule derives from, so adding a context is really one
config entry (verified behavior-preserving: same dep:check counts, same graph output).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 14:18:43 +02:00
ehoandClaude Sonnet 5 7ef8ac7409 chore: regenerate documentation.json
CI / frontend (push) Successful in 2m18s
CI / backend (push) Successful in 1m36s
CI / e2e (push) Successful in 3m20s
CI / storybook-a11y (push) Failing after 7m36s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 1m49s
Compodoc output regen — reflects the backend/frontend source changes from this
session (stamdata tables, Zgw/ client, IZaakSource seam).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:24:39 +02:00
ehoandClaude Sonnet 5 e38166061d docs(backlog): fix stale WP-27 undo/redo implementation claim
CI / backend (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / storybook-a11y (push) Canceled after 1m43s
CI / frontend (push) Canceled after 1m51s
WP-27's Decisions said undo/redo is a hand-rolled past/future: Brief[] stack in
BriefStore. WP-31 later extracted that into the shared createHistory<T> helper
(the backlog's own phase-7 ordering note says as much: "32 reuses the
createHistory helper 31 extracts") — BriefStore now calls .record/.undo/.redo on
it, same cap-50/push-on-edit/clear-on-new-edit semantics. Noted the supersession
rather than leaving the WP describing code that no longer exists verbatim.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:22:53 +02:00
ehoandClaude Sonnet 5 9c54b18144 docs(backlog): fix stale WP-19 CI description; add per-step model guidance
CI / frontend (push) Successful in 2m9s
CI / e2e (push) Canceled after 0s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / backend (push) Canceled after 1m10s
CI / storybook-a11y (push) Canceled after 3m19s
WP-19: the Files section still described the originally-planned CI approach
(background servers with & + wait-on); the actual implementation (documented in
ci.yml's own comment and playwright.config.ts) moved server lifecycle into
Playwright's webServer to fix the 2-hour e2e hang. Recorded as a Deviation entry
and corrected the Files section to match reality. Spot-checked the rest of the WP
(retryClicked wiring, the currentScenario() claim, the still-open retry gaps in
registration-detail/aanvraag-detail/brief) against current code — all still hold.

Session protocol: replace the removed model mandate with per-step guidance —
Opus for planning, Sonnet for implementation, Haiku subagents for simple
read-only CLI checks — so cost matches the step's difficulty, not the whole WP.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:19:28 +02:00
ehoandClaude Sonnet 5 c475384a63 docs(backlog): drop model-prescriptive session-protocol line
CI / backend (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / storybook-a11y (push) Canceled after 2m16s
CI / frontend (push) Canceled after 2m22s
Session protocol shouldn't dictate which model to use — WPs (including the new
WP-53/54) are written to be executable regardless of model.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-27 13:17:25 +02:00
ehoandClaude Opus 4.8 dfd64baba8 docs(backlog): add WP-53 (identity seam + citizen-scoping) and WP-54 (OpenZaak harness)
CI / frontend (push) Successful in 2m20s
CI / backend (push) Successful in 1m51s
CI / e2e (push) Successful in 3m20s
CI / storybook-a11y (push) Failing after 8m35s
CI / semgrep (push) Successful in 1m5s
CI / api-client-drift (push) Successful in 1m52s
The two highest-value OpenZaak roadmap gaps, each written self-contained (a "current
state" handoff section) so a fresh session can execute from the file + repo alone:

- WP-53: replace the stubbed owner/BSN with a real per-request CallerIdentity
  (pluggable stub, not DigiD), threading it into Authz, the ZGW JWT user claims, and
  a citizen-scoped read (rol__…__inpBsn). Production-blocking for a real deployment.
- WP-54: a separate docker-compose OpenZaak + scripted bootstrap + opt-in
  Category=Integration test — makes 50/51/52 developable against a live instance
  instead of only fixtures; kept out of the default gate.

Indexed in the backlog README (rows + phase-9 ordering note) and cited from
openzaak-integration.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 09:05:58 +02:00
ehoandClaude Opus 4.8 a37dfd47a4 ci: cap storybook-a11y resources + document the ACL learnings
CI / frontend (push) Successful in 3m22s
CI / backend (push) Canceled after 0s
CI / e2e (push) Canceled after 0s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / storybook-a11y (push) Canceled after 24s
- test-storybook:ci gets --maxWorkers=2 so the Jest runner stops spawning one
  headless Chromium per core and OOM-ing the Gitea runner host (the root cause).
- storybook-a11y job gains a container resource ceiling (--cpus=2 --memory=4g) as
  a belt-and-suspenders guardrail; noted it needs a docker-mode act_runner.
- openzaak-integration.md: add "Anti-corruption layer — two nested boundaries"
  teaching section (BFF ACL vs upstreams + FE ACL vs BFF, and the principles).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:21:28 +02:00
ehoandClaude Opus 4.8 1c3c195d32 feat(backend): expand stamdata + OpenZaak-ready cases seam (WP-49)
CI / frontend (push) Successful in 2m59s
CI / backend (push) Successful in 1m27s
CI / semgrep (push) Successful in 58s
CI / e2e (push) Successful in 2m30s
CI / api-client-drift (push) Canceled after 1m14s
CI / storybook-a11y (push) Canceled after 29m8s
Stamdata: add beroepen, opleidingen (temporal), and specialismen tables to the
schema-driven catalog (zero UI code). opleidingen.beroep and specialismen.beroep
both reference beroepen.code — the first stamdata->stamdata references, enforced by
two new StamdataRef entries in the CI gate.

OpenZaak/ZGW (WP-49, slice 1 — read-only zaken): introduce IZaakSource as the cases
read seam. Default LocalZaakSource reads the local SQLite store (offline); an
OpenZaakZaakSource (Zgw/ client: HS256 per-call JWT, ZGW->existing-DTO mapper,
paginating HTTP source) is selected behind Zgw:Enabled (default false). The FE never
changes — same ApplicationSummaryDto, no api-client drift. Unit-tested with fixtures
+ a stub HttpMessageHandler; no live OpenZaak needed.

Docs: ADR-0005, reference/openzaak-integration.md, WP-49..52 roadmap, stamdata.md
update, README index rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 15:01:06 +02:00
ehoandClaude Opus 4.8 cff711504f style: prettier-format document-feature SKILL.md
CI / frontend (push) Successful in 2m22s
CI / backend (push) Successful in 1m58s
CI / storybook-a11y (push) Failing after 6m38s
CI / semgrep (push) Successful in 1m7s
CI / e2e (push) Successful in 3m2s
CI / api-client-drift (push) Failing after 13m39s
Emphasis normalized to underscore; fixes red format:check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:18:14 +02:00
ehoandClaude Opus 4.8 ae61b68dd4 docs: index orphaned reference docs + add scaffolding guide
CI / frontend (push) Failing after 1m15s
CI / backend (push) Successful in 1m57s
CI / semgrep (push) Canceled after 0s
CI / api-client-drift (push) Canceled after 0s
CI / e2e (push) Canceled after 42s
CI / storybook-a11y (push) Canceled after 5m10s
- Index roles-and-access, dependencies, and dependency-graph (were on disk
  but missing from the docs README index).
- New reference/scaffolding.md: plop generators, gen:api (NSwag), gen:snippets,
  and the skill recipes — the one consolidated "how generation works" page.
- Fix stale backlog range (WP-01…WP-28 -> WP-48) and de-link the dead WP-32 row
  (undo folded into WP-31, no separate file).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 14:12:06 +02:00
ehoandClaude Opus 4.8 7ee4660d6e docs: regenerate dependency graph
CI / frontend (push) Failing after 1m22s
CI / backend (push) Successful in 1m41s
CI / e2e (push) Successful in 3m30s
CI / storybook-a11y (push) Failing after 6m18s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 2m8s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:57:12 +02:00
ehoandClaude Opus 4.8 cf69d474cd docs: reference guides for stamdata, audit log, feature flags + document-feature skill
CI / frontend (push) Failing after 1m31s
CI / backend (push) Successful in 1m48s
CI / e2e (push) Successful in 4m19s
CI / storybook-a11y (push) Failing after 7m21s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 3m1s
Three how-it-works/how-to-extend reference docs (docs/reference/), each with a
coupling section, indexed in docs/README.md. New document-feature skill so docs
ship in the same diff as the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:39:32 +02:00
ehoandClaude Opus 4.8 fbc4bf51d0 feat(beheer): stamdata deletion protection — CI referential gate + editor expire/warn
CI / frontend (push) Successful in 2m27s
CI / backend (push) Successful in 2m3s
CI / storybook-a11y (push) Failing after 6m30s
CI / semgrep (push) Successful in 1m5s
CI / e2e (push) Successful in 3m22s
CI / api-client-drift (push) Successful in 2m10s
CI gate (authoritative): generalize the dangling-reference test in StamdataValidationTests
into a declared, extensible reference list ("every declared reference into a stamdata key
resolves against the currently-valid stamdata"), starting with Diploma.Opleiding →
professions.program. Removing/renaming a referenced program, or expiring it while current
data still references it, fails the PR build (ADR-0004). Editor (fast feedback): confirm
before delete (warns a referenced row fails CI) and, for temporal tables, a "Sluiten per
vandaag" action that closes validity (geldigTot) — steering to expire over hard delete.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:35:17 +02:00
ehoandClaude Opus 4.8 67802c68b4 feat(admin): runtime feature flags (catalog-in-code, admin toggle, FE+backend)
Catalog declared in code (Domain/Features/FeatureFlags.cs, build-validated), on/off state
persisted in SQLite (FeatureFlagStore + migration). GET /flags (drives FE gating) + admin
PUT /admin/flags/{key} (new flags:manage capability + FlagsAdmin gate). Enforced end-to-end:
the `inschrijving-open` flag hides the Inschrijven nav item + dashboard action (FE) AND makes
POST /applications for a registratie 403 when off (backend). FE FeatureFlagStore mirrors
AccessStore (enabled() deny-by-default); admin toggle page at /beheer/functies in ADMIN_LINKS.
+4 backend tests, /me cap-list updated, client regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:29:48 +02:00
ehoandClaude Opus 4.8 ed264be714 fix(auth): persist session in localStorage so login survives the language switch
The language switch is a full-page navigation to a separate bundle (nl at /, en at
/en/); sessionStorage's per-tab semantics dropped the login across it. localStorage
is unambiguously shared same-origin and survives the hard navigation. Keeps G1 (naam
only, never the BSN). Trade-off: the demo session now survives tab close — a real
portal keeps auth in an httpOnly cookie/token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:18:33 +02:00
ehoandClaude Opus 4.8 c00e607b8f feat(i18n): nl at root URLs + keep devtools in the docker demo
Serve the source locale (nl) at / instead of /nl/: angular.json sourceLocale is now
{ code: 'nl', subPath: '' } → nl output at browser/ root (base href /), en stays /en/.
Rework localeLinks (nl → bare path, en → /en/…) + spec, and serve-i18n.mjs (nl assets at
root, en under /en/, / serves the nl index). And build the docker demo (+ serve:i18n) with
`--configuration development --localize` so isDevMode() stays true and the dev `⚙ state`
panel + role/scenario switchers render in the localized compose demo (they were correctly
gated off in the previous production build). Verified: nl base href /, en /en/, ngDevMode present.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:24:33 +02:00
ehoandClaude Opus 4.8 deb5d77e04 feat(dx): WP-43 — plop generators (value-object, form-machine)
CI / frontend (push) Successful in 3m11s
CI / backend (push) Successful in 2m27s
CI / storybook-a11y (push) Successful in 9m28s
CI / e2e (push) Successful in 4m38s
CI / semgrep (push) Successful in 1m20s
CI / api-client-drift (push) Successful in 2m13s
Runnable `npm run gen:value-object` / `gen:form-machine` (plop) that scaffold the two
pure-TS house patterns with a co-located spec: a branded value object + parseX (mirrors
postcode/bsn), and an Elm-style form/wizard machine (Draft/Valid/Errors + Editing/
Submitting/Submitted/Failed union + initial/pure reduce/assertNever). Prompts take
context + PascalCase name (positional-arg bypass); a post-action reminds to add the
English target for the generated $localize id. Templates in plop-templates/ (prettier-
ignored). Skills (value-object, form-machine) point at the generators. ui-component +
bff-endpoint stay skill-driven (Angular {{}} / backend + gen:api).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 18:04:54 +02:00
ehoandClaude Opus 4.8 00c5faacb9 feat(i18n): per-bundle LOCALE_ID + language switching under docker compose
CI / frontend (push) Successful in 3m18s
CI / backend (push) Successful in 2m28s
CI / storybook-a11y (push) Successful in 7m54s
CI / semgrep (push) Successful in 1m25s
CI / e2e (push) Successful in 3m54s
CI / api-client-drift (push) Successful in 2m10s
Fix the hardcoded LOCALE_ID: 'nl' — provide it from $localize.locale (the build-time
locale, 'nl'/'en', undefined→'nl' in dev) and register both nl+en locale data, so the en
bundle formats dates/numbers correctly. Make `docker compose up` serve the LOCALIZED build:
the web service now runs `ng build --localize` then serve-i18n.mjs, which gained a PORT env
+ an /api reverse-proxy (API_PROXY_TARGET → the api container) so both /nl/ and /en/ are
served with the language switcher working end-to-end. Drop the now-unused proxy.conf.docker.json
(serve-i18n proxies /api itself); update ARCHITECTURE. `npm start` stays the nl-only HMR loop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:51:46 +02:00
ehoandClaude Opus 4.8 92f825242a feat(i18n): CIBG "Taal instellen" language switcher
CI / frontend (push) Successful in 2m47s
CI / backend (push) Successful in 2m29s
CI / storybook-a11y (push) Successful in 8m12s
CI / e2e (push) Successful in 3m55s
CI / semgrep (push) Successful in 1m11s
CI / api-client-drift (push) Successful in 2m13s
Add a language switcher matching the CIBG Taal-instellen pattern: a <nav> region
(sr-only heading + aria-label) with an endonym link per locale (lang/hreflang, the
active one aria-current + non-link), mounted right after the skip link in the shell.
Compile-time $localize means each locale is its own bundle under /<locale>/, so the
switch is a full navigation to the sibling bundle — active locale read from the baked
base href. Pure localeLinks() (+spec) builds path-preserving targets. Since `ng serve`
serves nl-only at /, add `npm run serve:i18n` (localized build + a tiny static server
with per-locale SPA fallback) so the switch is demoable. +story.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:21:55 +02:00
ehoandClaude Opus 4.8 ee2413f2fe feat(brief): besluit guidance + read-only notice (role clarity)
Surface the besluit-driven assistance that was previously silent: a pure
besluitGuidance() (kern passage count + needs-reason flag) rendered as a hint below
the besluit panel in behandel-scherm (warning to pick a reden, else info on how many
standaardteksten were inserted). Add a read-only notice on letter-composer for a pure
viewer (no edit/approve/reject/send right, e.g. admin) so the read-only letter isn't
mistaken for a broken editor — the "reverted to a view of the letter" confusion was a
role issue (non-drafter sees the composer). +besluitGuidance spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 17:14:10 +02:00
ehoandClaude Opus 4.8 2bb16d1161 fix(dev): dev switcher reflects the current role/scenario in the dropdowns
The WP-33 panel's <select [value]> was applied before the @for options existed, so
the matching option was never selected (dropdown always showed the first item).
Move selection to [selected] on each <option>, and read role/scenario via getters
(currentRole()/currentScenario()) so the dropdowns reflect the active value whenever
the panel opens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:18:18 +02:00
ehoandClaude Opus 4.8 8cd925717f feat(beheer): admin audit view at /beheer/audit (finishes WP-42)
The WP-41 GET /admin/audit trail now has an FE view: a beheer audit page (domain
AuditEntry + adapter/parse + store) rendering the data-minimised trail as a read-only
table, capability-gated on cases:manage. Added to ADMIN_LINKS (header nav + dashboard
Beheer section) and to the role.interceptor ROLE_AWARE list so the admin-gated call
carries X-Role. Closes WP-42's audit half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:05:36 +02:00
ehoandClaude Opus 4.8 0f30143c5d feat(privacy): WP-41 — persisted, queryable authz/PII-reveal audit
Persist the security-relevant events (authz denials + BIG-nummer reveal/step-up) into a
data-minimised EF table (AuthzAuditEntry: At/Action/Resource/Decision/Role/CorrelationId —
never a name/BSN/value), extending the DocumentStore AuditEntry pattern (migration AuthzAudit).
AuditAuthz now persists via AuthzAuditStore.Record alongside its log line. GET /admin/audit
(admin-gated by the existing CasesAdmin) returns the trail newest-first. +3 backend tests incl.
a schema-carries-no-PII reflection test. Typed client regenerated (audit() + AuthzAuditDto);
no FE consumer yet (a future audit view must add the ROLE_AWARE prefix). Finishes WP-42's audit half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:53:33 +02:00
ehoandClaude Opus 4.8 8c54ede6eb feat(showcase): PII section — mask + elfproef parse (WP-42, mask/parse half)
CI / storybook-a11y (push) Successful in 5m3s
CI / frontend (push) Successful in 1m53s
CI / backend (push) Successful in 1m26s
CI / e2e (push) Successful in 2m46s
CI / semgrep (push) Successful in 59s
CI / api-client-drift (push) Successful in 2m7s
Add a "PII — maskeren & parsen" section to /concepts demonstrating the WP-40 pieces
with FP + atomic design, framed for AVG art. 9 / data-minimisation: a live
<app-masked-value> atom (masked BSN that reveals on click; real reveal is step-up +
audited in behandel-scherm) and a live parseBsn elfproef parse. Both show the real
linked source via the WP-39 snippet mechanism (new showcase regions in bsn.ts + pii.ts,
registered in gen-snippets.mjs). Delivers WP-42's showcase demo; the persisted-audit
half stays pending WP-41.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:43:53 +02:00
ehoandClaude Opus 4.8 62cb34b60f feat(privacy): WP-40 — PII kernel (Bsn value object + masked-value atom)
Branded Bsn value object with the elfproef (11-test) checksum in shared/kernel/bsn.ts,
wired into the DigiD login boundary so login does real BSN validation (hint + e2e BSNs
updated to a valid 123456782). Consolidate the pure maskers into shared/kernel/pii.ts
(maskBsn/maskTail/REDACTED); debug-state keeps redactProfile (needs the registratie
BigProfile — boundary). New <app-masked-value> atom (+story) centralises the masked
`.includes('*')` detection + reveal affordance; behandel-scherm refactored onto it.
Session.bsn stays string (persistence boundary drops it for privacy). +specs for bsn/pii.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:24:59 +02:00
ehoandClaude Opus 4.8 19f2e9b734 feat(showcase): WP-39 — linked code snippets + teaching animations
CI / frontend (push) Successful in 2m5s
CI / storybook-a11y (push) Successful in 5m56s
CI / backend (push) Successful in 1m30s
CI / e2e (push) Successful in 2m51s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 2m6s
Anti-drift snippets: `?raw` isn't supported by Angular's esbuild build, so real code
is exposed via // #region showcase:<name> markers in source (registration/remote-data/
postcode/change-request.machine/intake.machine) → scripts/gen-snippets.mjs → committed
snippets.generated.ts, with a CI drift gate so the shown code is the shipped code. The
/concepts page renders the 5 real snippets (union/fold/parse/machine/steps) with a source
caption + a tiny dependency-free highlightTs (+spec); deliberately-wrong illustrations stay
authored. Teaching motion: active state node pops on transition + parse result animates in
(existing reduced-motion-safe toolkit). Generated file prettier-ignored (like api-client).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:49:52 +02:00
ehoandClaude Opus 4.8 f8cd77a48a feat(test): WP-46 — Vitest coverage (report-only)
Enable coverage via the @angular/build:unit-test builder's first-class options: a
`coverage` configuration on the test target (v8 provider, text-summary/html/lcov,
excludes for specs/stories/contracts/generated client) + `npm run test:coverage`.
Report-only (no thresholds) — visibility first. CI's test step now runs test:coverage
(strict superset: fails on a failing test, never on coverage). Baseline ~71% stmts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:23:55 +02:00
ehoandClaude Opus 4.8 7d2a36ff22 feat(arch): WP-38 — dependency graph + declarative boundaries (dependency-cruiser)
CI / frontend (push) Successful in 2m11s
CI / storybook-a11y (push) Successful in 5m46s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 2m55s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 2m5s
Adopt dependency-cruiser as the single declarative source for bounded-context +
atomic-layer boundaries, replacing the per-context no-restricted-imports blocks that
had to be hand-copied (and had left herregistratie uncovered). `.dependency-cruiser.js`
encodes context direction (everyone→shared, herregistratie→registratie, showcase→*),
domain-purity, contracts-import-nothing, ui↛infrastructure, ApiClient confinement, and
no-circular. `npm run dep:check` enforces (wired into ci-local.sh + the frontend CI job);
`npm run dep:graph` emits a committed mermaid context×layer graph. ESLint slimmed to
no-explicit-any + template a11y. Docs + new-context skill updated to the single source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:51:04 +02:00
ehoandClaude Opus 4.8 fe9e3121c7 fix(dev): WP-37 — dev-switcher resets scenario/role instead of sticking
CI / frontend (push) Successful in 1m49s
CI / storybook-a11y (push) Successful in 4m59s
CI / backend (push) Successful in 1m27s
CI / e2e (push) Successful in 2m59s
CI / semgrep (push) Successful in 59s
CI / api-client-drift (push) Successful in 2m9s
currentScenario()/currentRole() read the URL param before sessionStorage, so a
stale ?scenario=/?role= in the address bar overrode the switcher on reload
("stuck on slow"). The switcher now strips both dev params from the URL
(pure stripDevParams + history.replaceState) before reloading, so the stored
value wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:34:01 +02:00
ehoandClaude Opus 4.8 a828e604d1 docs(backlog): add Phase 8 — platform/DX/showcase (WP-37..46)
New phase from the 5-item analysis: linked showcase snippets + animations,
runnable scaffolding generators (pages/wizards/context/SSP), dependency graph +
declarative boundaries, PII hardening (branded Bsn VO + masked-value atom +
persisted no-PII audit) doubling as a privacy showcase, and Vitest coverage.
Goal: both a teaching showcase and a production starter template. Sequenced with
priorities + dependencies in the README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:34:01 +02:00
ehoandClaude Opus 4.8 a5ac1fb729 docs: roles & ABAC quick reference
CI / frontend (push) Successful in 1m52s
CI / storybook-a11y (push) Successful in 5m8s
CI / backend (push) Successful in 1m30s
CI / e2e (push) Successful in 2m59s
CI / semgrep (push) Successful in 59s
CI / api-client-drift (push) Successful in 2m5s
Add docs/reference/roles-and-access.md — a short, scannable reference for the
three dev roles (drafter/approver/admin), that login is faked and role is a
separate dev-only stand-in, how to switch (dev switcher / ?role=), a per-role
capability table, and the one ABAC principle (server emits + enforces; UI
renders). Points to PRD-0002 + the code. Adds a pointer from CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:03:05 +02:00
ehoandClaude Opus 4.8 63fdacf622 feat(dashboard): admin "Beheer" section linking the admin pages
CI / frontend (push) Has been cancelled
CI / backend (push) Has been cancelled
CI / semgrep (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
Add a capability-gated Beheer section to the dashboard listing the admin pages
(Huisstijl, Stamdata, Aanvragen), each shown when the principal holds its
capability — so admin pages are discoverable, not URL-only. Extract the admin
link list to shared/layout/admin-links.ts as one source of truth, reused by the
site header (was ADMIN_NAV_ITEMS) and the new section. Capability-gated, never
role-derived (PRD-0002).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:51:00 +02:00
ehoandClaude Opus 4.8 5b6023045e ci(semgrep): install with --ignore-installed (apt-managed PyJWT can't be uninstalled)
The runner's semgrep install failed trying to replace Debian's apt-managed PyJWT
("Cannot uninstall PyJWT ... RECORD file not found"). --ignore-installed installs
semgrep's deps fresh without uninstalling the apt copies, staying within the
runner's constraints (no setup-python, no container job).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:51:00 +02:00
ehoandClaude Opus 4.8 446ea9474b feat(registratie): WP-36 — admin cases page + admin delete
Admin-only overview of all cases across owners + an admin delete, gated by a new
`cases:manage` capability (Authz role→cap + CanManageCases + CasesAdmin gate;
FE capability + guard + nav + role.interceptor prefix — the org-template/stamdata
recipe). Backend adds ApplicationStore.ListAll()/DeleteAny() and GET /admin/cases +
DELETE /admin/cases/{id}; admin delete removes ANY case incl. submitted. Page lives
in registratie/ui (owns the Aanvraag aggregate; reuses aanvraag-view + parse),
routed /beheer/zaken; delete guarded by a native confirm, optimistic with rollback.
Typed client regenerated (documents the new endpoints + owner field).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 12:23:34 +02:00
ehoandClaude Opus 4.8 d1abd35b0d feat(registratie): WP-35 — one Concept per case type (server-enforced)
CI / semgrep (push) Has been cancelled
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 5m17s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m1s
CI / api-client-drift (push) Successful in 2m4s
Make "at most one unsubmitted Concept per type" a server invariant instead of a
client-only convenience. ApplicationStore.Create → CreateConcept guards atomically
under the write gate and POST /applications returns 409 when a duplicate would be
created. The FE draft-sync recovers from the 409 by adopting the existing Concept
(ensureId → findConcept) rather than erroring — one-per-type means the second
attempt lands on the existing draft. Typed client regenerated (documents the 409).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:07:11 +02:00
ehoandClaude Opus 4.8 57f6f2f8d8 style(registratie): WP-34 — BRP address as datablock, phone in grey fieldset
CI / frontend (push) Successful in 1m53s
CI / storybook-a11y (push) Successful in 4m51s
CI / backend (push) Successful in 1m26s
CI / e2e (push) Successful in 2m51s
CI / semgrep (push) Failing after 28s
CI / api-client-drift (push) Successful in 2m10s
Restyle the contact-change form with house CIBG surfaces instead of hand-rolled
markup: the read-only BRP address now uses app-data-block/app-data-row (grey
datablock), and the phone field sits in a plain <fieldset> inside the existing
.form-horizontal form (inherits the CIBG grey box). No new CSS surfaces; row keys
reuse the address.* i18n ids. Logic/machine/specs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:05:40 +02:00
ehoandClaude Opus 4.8 0ea43af7b6 feat(registratie): WP-34 — phone field + BRP address read-only
Reshape the adreswijziging form into a contact-change form: the BRP address is
authoritative and shown read-only (you change it at the gemeente), and the phone
number becomes the editable/submittable field. New Telefoonnummer value object
(parse-don't-validate); backend RejectPhoneChange re-validates as authority.
POST /change-requests now carries { telefoon } (typed client regenerated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:11:26 +02:00
ehoandClaude Opus 4.8 1ed4850858 feat(dev): WP-33 — in-app dev switchers for scenario + role
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 27m52s
CI / backend (push) Successful in 1m24s
CI / e2e (push) Successful in 27m24s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Successful in 2m7s
Surface the ?scenario= and ?role= dev stand-ins as dropdowns in the existing
debug-state devtool, so a demo can flip them with a click instead of editing the
URL. scenario.ts/role.ts gain set* setters + exported valid-value lists (reused
by the panel, no duplicated source of truth); scenario becomes tab-sticky like
role so it survives navigation. Applied via location.reload() since both are
read per-request in interceptors. Extends the debug-state eslint exemption to the
ui→infrastructure rule (same devtool precedent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:53:26 +02:00
ehoandClaude Opus 4.8 610ff7c1cd docs(backlog): mark WP-31/32 done
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:45:28 +02:00
ehoandClaude Opus 4.8 4cf1147fc1 feat(beheer): WP-32 — undo/redo for the stamdata table editor
CI / frontend (push) Successful in 1m50s
CI / backend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / semgrep (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
Wire the WP-31 createHistory helper into StamdataStore: per-table undo/redo
over the edited rows, recording only real edits and restoring via the existing
Seed msg. Ctrl/Cmd+Z / +Shift+Z, deferring to native text-undo inside grid
cell inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 16:10:27 +02:00
ehoandClaude Opus 4.8 ac3e9a9399 refactor(fp): WP-31 — shared store helpers (dedupe brief/org-template/stamdata)
Audit "apply high-value": extract four shared helpers into shared/application/ and
rewire the editor stores (behaviour unchanged, existing specs are the gate):
- action-state.ts: ActionState/SaveState (were duplicated in both brief stores).
- history.ts: createHistory<T> (extracted from BriefStore's WP-27 undo/redo; WP-32 reuses).
- debounced-save.ts: createDebouncedSave (the 600ms timer/PendingSave dance, was 2×+).
- machine-remote-data.ts: machineRemoteData (the loading/failed/loaded→RemoteData switch, 3×).
Each helper has a co-located spec. Deferred DDD findings (contracts/ inconsistency, a
parse* traverse combinator, the 6× Seed boilerplate) are reported in the WP file, not built.

npm run ci green; 323 tests (+13 helper specs); brief/org-template/stamdata specs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:40:02 +02:00
ehoandClaude Opus 4.8 13b3e5e663 docs(backlog): truth-up statuses + add Phase 7 (WP-31..36)
WP-25 (1bb9383) and WP-27 (5e36d68) were done but the table still said todo — fixed.
WP-28/WP-30 remain todo. Added Phase 7 (refinements & hardening): WP-31 shared store
helpers, WP-32 stamdata undo, WP-33 dev switchers, WP-34 adres phone + BRP read-only,
WP-35 one Concept per type, WP-36 admin cases page + delete. Only 31→32 is ordered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 15:26:55 +02:00
ehoandClaude Opus 4.8 9b2a878933 ci(semgrep): install via runner python3/pip, drop container + setup-python
CI / api-client-drift (push) Failing after 30s
CI / frontend (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 4m50s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 3m0s
CI / semgrep (push) Failing after 29s
The container: approach failed — this act_runner times out pulling its base runner
image (docker.gitea.com/runner-images:ubuntu-latest, IPv6) for container jobs. And
the earlier setup-python step failed downloading Python. Both avoided: run on the
plain ubuntu-latest runner and install semgrep with the preinstalled python3/pip
(`python3 -m pip install --break-system-packages semgrep`; --break-system-packages
survives PEP-668, pip puts semgrep on PATH). Verified in a clean python:3.12
container that pip install lands `semgrep` on PATH and the scan runs. Still
report-only; WP-30 tracks the flip to --error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 10:10:57 +02:00
ehoandClaude Opus 4.8 b34cc2ba3b ci(semgrep): run in the official Semgrep container (setup-python failed on the runner)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 31s
CI / backend (push) Failing after 31s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 25s
CI / api-client-drift (push) Failing after 30s
The setup-python + `pip install semgrep` step errored on the Gitea runner. Switch to
`container: docker.io/semgrep/semgrep` (semgrep preinstalled) — the documented way to
run Semgrep in CI, and the exact execution verified locally (306 rules / 450 files,
27 findings, exit 0). Fully-qualified image name so short-name resolution works under
Docker or podman. Still report-only (no --error); WP-30 tracks flipping to blocking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:46:48 +02:00
ehoandClaude Opus 4.8 c404995980 ci: replace CodeQL with Semgrep (Gitea-compatible SAST)
CI / frontend (push) Failing after 31s
CI / storybook-a11y (push) Failing after 30s
CI / backend (push) Failing after 30s
CI / e2e (push) Failing after 30s
CI / semgrep (push) Failing after 30s
CI / api-client-drift (push) Failing after 30s
CodeQL is GitHub-only — its analyze step uploads SARIF to GitHub's code-scanning
API and assumes a GitHub Security tab; this CI runs on Gitea only, so the job could
never go green (it had been red since it was added). Replace it with Semgrep OSS, a
plain CLI SAST with no account/platform API, which runs fine on Gitea.

- Remove the codeql job (+ its security-events permission) and the schedule trigger
  (it existed only for codeql; semgrep runs on push + PR).
- Add a semgrep job: setup-python + `pip install semgrep` +
  `semgrep scan --config p/default --config p/csharp --metrics=off`. pip-on-runner
  (not container:) mirrors the other jobs' model; anonymous registry, telemetry off.
- Report-only for now (no --error → job stays green): a local dry-run found 27
  findings, mostly CI/config policy (unpinned actions, .npmrc), not app-code vulns.
  WP-30 tracks triaging them + flipping to --error (a blocking gate).

Verified locally: `semgrep scan` runs clean (exit 0 without --error, 306 rules /
450 files). CI behaviour confirmable only on the Gitea runner — watch the run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 09:34:13 +02:00
ehoandClaude Opus 4.8 f7417ee4e9 fix(storybook): stub AccessStore in the shell story (fixes storybook-a11y red)
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 4m27s
CI / backend (push) Successful in 1m23s
CI / e2e (push) Successful in 2m36s
CI / codeql (csharp) (push) Failing after 2m1s
CI / codeql (javascript-typescript) (push) Failing after 1m29s
CI / api-client-drift (push) Successful in 2m8s
The header now injects AccessStore (→ MeAdapter → ApiClient) for its admin links.
f719676 stubbed that in the site-header story but missed shell.stories.ts, which
renders the header via ShellComponent — with only provideRouter it threw NG0201
(no ApiClient provider), failing the Shell smoke-test and turning storybook-a11y
red on CI (deterministic, not the local worker-contention timeouts). Stub
AccessStore there too (can() → false, no admin links).

Verified: full test-storybook suite single-worker now 60 suites / 170 tests green.
Lesson: when a shared component gains a service dep, every story that renders it —
directly OR via a parent template — needs the provider.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 21:21:01 +02:00
ehoandClaude Opus 4.8 f7196768ea fix(auth): make admin pages reachable — async capability guard + sticky dev role + nav
CI / storybook-a11y (push) Failing after 4m28s
CI / frontend (push) Successful in 1m44s
CI / backend (push) Successful in 1m28s
CI / e2e (push) Successful in 2m49s
CI / codeql (csharp) (push) Failing after 2m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m6s
The admin pages (/beheer/stamdata, /brief/huisstijl) were unreachable in the browser,
for three compounding reasons — all fixed here:

- **Guard raced /me.** capabilityGuard read can() synchronously while /me was still
  loading, so it denied even an entitled admin (deny-by-default) and bounced to /login.
  It's now async: awaits AccessStore.whenReady() (new — resolves once /me settles), then
  allows if entitled; an authenticated-but-unentitled user goes to /dashboard, anonymous
  to /login. + auth.guard.spec (the missing test that let this ship).
- **Dev role wasn't sticky.** currentRole() read ?role= from the URL on every request,
  but login/nav drop the param, silently reverting admin→drafter mid-session and 403-ing
  the admin endpoints. It now persists the role per-tab (sessionStorage), so every
  role-aware request keeps it. Dev-only (the interceptor is wired only under isDevMode).
- **No way in.** Added capability-gated Huisstijl + Stamdata links to the header (shown
  only when /me grants the cap); injecting AccessStore there also warms /me early. New
  en translations for the two labels; site-header story stubs AccessStore (+ AsAdmin
  variant) so it needs no HTTP.

Verified live: with ?role=admin the header shows both links, clicking Stamdata loads the
grid (GET /api/v1/stamdata → 200, was 403→redirect); a non-admin sees no link. Full
`npm run ci` green (310 tests); site-header stories pass axe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 21:03:32 +02:00
ehoandClaude Opus 4.8 c0834cdbce fix(beheer): send X-Role to /api/v1/stamdata (admin reads were 403)
The stamdata admin editor's adapter calls /api/v1/stamdata via the generated
ApiClient → roleInterceptor, but ROLE_AWARE omitted /api/v1/stamdata, so no X-Role
was sent and the backend StamdataAdmin gate resolved Drafter → 403 on every read
(confirmed: 403 without X-Role, 200 with X-Role: admin). Added /api/v1/stamdata to
the allow-list (same class of gap WP-23 fixed for /me) + a roleInterceptor spec so
the next admin endpoint isn't forgotten.

Note: a separate issue still blocks the page in the browser — capabilityGuard
redirects both admin routes to /login because it checks can() before /me resolves;
tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:43:01 +02:00
ehoandClaude Opus 4.8 7d3a63a7a5 docs(backlog): WP-30 — CI performance follow-ups
CI / frontend (push) Successful in 1m47s
CI / storybook-a11y (push) Successful in 5m9s
CI / backend (push) Successful in 1m35s
CI / e2e (push) Successful in 2m54s
CI / codeql (csharp) (push) Failing after 2m12s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m8s
Capture the deferred CI options from the speed pass (708d4c2) so they aren't lost:
node_modules cache to skip npm ci, smaller CI runner image (act_runner label→image,
needs admin; or repo-only container: for node-only jobs), path-filtered jobs, a fast
split lint job, and an optional lean prod backend image. Notes the constraints (CI not
observable from the agent env; compose images aren't used by CI). Added the README row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:32:18 +02:00
ehoandClaude Opus 4.8 708d4c2308 ci: speed up pipeline + shrink demo web image
CI / frontend (push) Successful in 1m46s
CI / backend (push) Has been cancelled
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / storybook-a11y (push) Has been cancelled
CI (Gitea Actions, .github/workflows/ci.yml):
- CodeQL off the PR critical path: runs on push-to-main + a weekly cron only
  (`if: github.event_name != 'pull_request'`, `schedule: Mondays 03:00 UTC`).
  The 2-language 20-min matrix was the slowest thing on every PR; code is still
  scanned on main + weekly.
- Cache Playwright browsers (~/.cache/ms-playwright) in the storybook-a11y + e2e
  jobs — skips the chromium download on a hit; `install --with-deps` then only does
  the fast apt deps check.
- Cache NuGet (~/.nuget/packages, keyed on **/*.csproj — no packages.lock.json) in
  the backend / e2e / api-client-drift / codeql-csharp jobs.
- `npm ci --prefer-offline --no-audit --no-fund` in the 4 npm jobs.

Demo (docker-compose.yml, local only — NOT used by CI): web image node:24 →
node:24-slim (~1.1GB → 232MB verified). The container only runs `npm ci && ng serve`
and the native deps ship prebuilt glibc binaries, so slim needs no toolchain —
verified: npm ci clean, ng serve boots, app returns 200.

Note: CI timing/behaviour can only be confirmed on the Gitea runner (not observable
locally). Every change here is independently revertable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:28:19 +02:00
ehoandClaude Opus 4.8 2820cd3476 fix(ci): e2e — let Playwright manage both servers (fixes the 2-hour hang)
CI / e2e (push) Successful in 2m50s
CI / frontend (push) Successful in 1m49s
CI / storybook-a11y (push) Successful in 4m55s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 2m3s
CI / codeql (javascript-typescript) (push) Failing after 1m29s
CI / api-client-drift (push) Successful in 1m42s
The e2e job backgrounded `dotnet run &` and `ng serve &` in separate Actions
steps, then `npx wait-on` (no timeout) in a later step. A process started with
`&` in one step is killed when that step's shell exits, so wait-on waited forever
on servers that were already gone — the job hung until the runner's hard limit
(~2h; Gitea's act_runner doesn't reliably enforce timeout-minutes).

Move both servers into Playwright's `webServer` (an array: backend + `npm start`),
so Playwright starts them, waits for readiness, runs the suite, and tears them
down in the one `npm run e2e` process — CI and local alike. The CI e2e job is now
just npm ci / playwright install / npm run e2e. `reuseExistingServer` is on locally
(reuses a running app, incl. the docker stack) and off in CI (fresh start).

Verified locally via `CI=1 npm run e2e` against free ports + a clean db: both
smoke tests pass in ~15s (no hang). The earlier local failure was leftover SQLite
state (WP-22) from a dirty run resuming a Concept — CI checks out fresh, so its db
is always empty.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:07:35 +02:00
ehoandClaude Opus 4.8 6fa155caa3 fix(ci): green build — en translations, quiet docker npm, local CI check
CI / frontend (push) Successful in 1m51s
CI / storybook-a11y (push) Successful in 5m4s
CI / backend (push) Successful in 1m21s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Three fixes so Gitea CI is green and stays that way:

- **ng build --localize**: `messages.en.xlf` (hand-maintained, matched by id) had drifted
  ~93 units behind the source since WP-24 — `beheer.*`, `brief.canvas.*`, `brief.diff.*`,
  `orgTemplate.*`, `brief.besluit.*`, etc. With `i18nMissingTranslation: "error"` that fails
  the frontend job. Added English `<target>`s for every missing unit (interpolation `<x/>`
  tags preserved); both nl + en bundles build clean.
- **docker npm noise**: the web container's `npm ci` now runs `--no-fund --loglevel=error`,
  so `docker compose up` boots without npm 11's deprecation / allow-scripts / funding
  warnings (verified in the running container: 0 warning lines). The underlying Angular
  webpack-builder deprecation is upstream; migrating to @angular/build is out of scope.
- **local CI check**: `scripts/ci-local.sh` + `npm run ci` mirror the CI jobs (lint,
  format:check, check:tokens, test, build --localize, audit, backend format+test,
  api-client drift; `--full` adds storybook-a11y) so a red build is caught before pushing.
  Opt-in pre-push hook at scripts/githooks/pre-push. Documented in CLAUDE.md.

Verified: `npm run ci` passes end-to-end; storybook-a11y 169/169 (the local parallel-worker
timeouts were the docker stack starving chromium, not a regression).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:56 +02:00
ehoandClaude Opus 4.8 5761b13dd2 style: format the repo with prettier (green format:check)
`npm run format:check` (a CI gate) had drifted red across 44 files — pre-existing
files plus recently-added ones committed without formatting. Ran `prettier --write .`;
no logic changes. Also regenerates documentation.json (compodoc reflects the reformatted
component sources).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 17:20:42 +02:00
ehoandClaude Opus 4.8 7dfbd4501f docs: fix ARCHITECTURE.md mermaid diagrams + document the autosave last-mile guard
CI / frontend (push) Failing after 1m6s
CI / storybook-a11y (push) Successful in 5m3s
CI / backend (push) Successful in 1m20s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Rewrite all five mermaid blocks to portable syntax so they render (verified via
a mermaid-native render): split two `classDef`/`class` statements that shared a
line (the "why not just signals" graph); drop the `reduce() — PURE` participant
alias and the `;` inside a Note (§2c sequence); remove `<br/>` and parenthetical
transition labels from the two stateDiagram-v2 blocks (RemoteData, intake),
moving the aside into a note; normalize `<br/>`→`<br>` and drop `<b>` in the
atomic-hierarchy flowchart.

Extend §2g to document the now-closed gap: a CanDeactivate guard flushes the
pending debounce before in-app navigation, and beforeunload warns + best-effort
flushes on hard close (noting the HttpClient/keepalive limitation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:29:09 +02:00
ehoandClaude Opus 4.8 645fad088e feat(fp): flush pending autosave before navigation/unload
Close the last-mile autosave gap: a debounced edit made in the final <600ms
before leaving a page was lost — the wizard draft-sync timer is cleared on
destroy without flushing, and root stores keep an armed timer the teardown
ignores.

New `shared/application/pending-saves.ts`: a root `PendingSaves` registry every
autosave owner joins (BriefStore, OrgTemplateStore, each createDraftSync). Two
seams flush through it — `flushPendingGuard` (CanDeactivate, on the five
autosave routes) awaits the pending write before an in-app route change; a
`beforeunload` handler (provideUnloadFlush) fires it best-effort and raises the
browser's native unsaved-changes prompt. ponytail: the HTTP seam is Angular
HttpClient (no keepalive/sendBeacon), so a hard-close flush can't be guaranteed
— hence the prompt; upgrade path noted in a comment. Each owner now nulls its
timer handle on fire so `hasPendingSave()` is accurate, and exposes
`flushPending()`.

Verified live against the running stack: navigating away 91ms after a keystroke
(well inside the debounce) fires one PUT /brief before the route changes; a
dirty reload raises the prompt, a clean reload does not. FE lint / check:tokens
/ 299 tests (+11) / build / build-storybook green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 16:29:09 +02:00
ehoandClaude Opus 4.8 e5edae4970 fix(a11y): letter-canvas "added" diff badge contrast (WP-27)
The green "nieuw" badge used dark text on groen-500 (#39870c) = 3.41:1,
failing WCAG AA 4.5:1 (axe, WithDiff story). Dark text passes on the orange
"changed" badge (4.79:1) but no green passes it. Give the added badge white
text on groen-700 (#176e1b) = 6.4:1; the changed badge is unchanged. The
prior comment's blanket "dark text, not white" was correct for orange but
wrong for green. Full storybook axe gate now green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:55:24 +02:00
ehoandClaude Opus 4.8 5968ef9030 docs: ADR-0004 stamdata data-file + WP-29 stamdata beheer editor
Amend ADR-0004 to record what shipped in 0e77faf: a table may live as typed
C# or as a typed JSON data-file (the compile-time value check traded for
editor ergonomics, caught by StamdataValidationTests instead); valid-time and
the generic StamdataCatalog/StamdataTable model; the low-code PR-emitting
editor is no longer "future". Add WP-29 backlog file (done) + README row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:44:01 +02:00
ehoandClaude Opus 4.8 0e77faf351 feat(stamdata): admin stamdata maintenance editor (beheer)
Realizes ADR-0004's "future low-code editor that commits a PR": an
admin-only stamdata maintenance editor built on the stamdata-as-code
foundation.

Backend: `professions` moves from a hardcoded C# dictionary to an embedded
`professions.json` data-file (typed as `ProfessionMapping`) with valid-time
(geldigVan/geldigTot, half-open). A generic, reflection-driven
StamdataCatalog/StamdataTable/StamdataFile describes every table so one
endpoint pair + one grid editor serve all of them; add a table in one line.
Two read-only, admin-gated endpoints (GET /stamdata, GET /stamdata/{table}
?peildatum=) — no runtime write path. Generic build gate
`Every_catalog_table_is_valid` (keys non-blank, no overlapping validity,
well-formed windows).

Frontend: new `beheer` context (route beheer/stamdata, capabilityGuard
'stamdata:edit'). A schema-driven grid editor edits rows locally; download()
emits {table}.json for the admin to commit as a reviewed PR (no mutation
command — the CI build + StamdataValidationTests stay the authority).

Full gate GREEN both sides; gen:api leaves no drift; new stamdata story
passes axe. See WP-29 + ADR-0004.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 13:43:51 +02:00
ehoandClaude Opus 4.8 c459fa0a60 feat(stamdata): extract policy-question text into Stamdata
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / frontend (push) Failing after 1m21s
CI / storybook-a11y (push) Failing after 5m27s
CI / backend (push) Successful in 1m35s
CI / codeql (csharp) (push) Failing after 1m50s
CI / api-client-drift (push) Successful in 2m1s
CI / e2e (push) Failing after 3h14m48s
Move the geldigheidsvragen wording out of DiplomaRules into
Stamdata.PolicyQuestions (business-editable text, config-as-code); DiplomaRules
keeps only the rule of which questions apply. Extend StamdataValidationTests
(no blank id/wording, distinct ids in the manual set) and update ADR-0004.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:46:38 +02:00
ehoandClaude Opus 4.8 a7f737e18c docs: ADR-0004 stamdata-as-code
CI / frontend (push) Failing after 1m20s
CI / storybook-a11y (push) Failing after 5m27s
CI / e2e (push) Has been cancelled
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / backend (push) Has been cancelled
Document the config-as-code strategy for business-tunable reference data:
typed checked-in config validated at compile time (never a production DB),
where UI text (), reference tables (Stamdata/), and letter content
each live, and why org-templates are the deliberate runtime-editable exception.
Index it in docs/README.md and add a CLAUDE.md pointer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:41:21 +02:00
ehoandClaude Opus 4.8 fa7e9c5cfb feat(stamdata): profession↔diploma map as validated config-as-code
Extract the profession↔diploma table out of DiplomaRules into a dedicated
Stamdata.Professions module (business-editable data, separated from the rules
that consume it) and add StamdataValidationTests as the build-time gate: every
seeded diploma program must resolve to a real profession, no blank entries. A
bad edit now fails the build instead of silently rendering "Onbekend". Rules
and behaviour unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 07:41:04 +02:00
ehoandClaude Opus 4.8 14210fa2b0 docs: document form autosave + FE↔BE request lifecycle in ARCHITECTURE
New §2g explains field persistence (keystroke → model → 600ms snapshot
debounce; blur only marks touched, never saves) and §6a refreshes the stale
backend section with the real request lifecycle (NSwag client, httpClientFetch
seam, read/write traces) — both with relative links to the source files.
Add discovery pointers from the learning path (lesson 2.2 and the capstone).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:38:49 +02:00
ehoandClaude Opus 4.8 989a32acb4 docs: add Foundations learning path — paced 3-day onboarding route
CI / e2e (push) Waiting to run
CI / frontend (push) Failing after 1m4s
CI / storybook-a11y (push) Failing after 4m40s
CI / backend (push) Successful in 1m19s
CI / codeql (csharp) (push) Failing after 1m49s
CI / codeql (javascript-typescript) (push) Failing after 1m28s
CI / api-client-drift (push) Failing after 1m22s
New src/docs/learning-path.mdx sequences the existing Foundations docs into
self-contained, bite-size lessons (Day 1 orient / Day 2 functional core /
Day 3 quality + capstone), each with goal, time, exercise, and self-check.
Insert it into the storySort order after Overview, add a pointer from the
Overview hub, and fix a stale cibg cross-link in atomic-design.mdx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:24:46 +02:00
ehoandClaude Opus 4.8 35a9d18374 docs: regenerate Storybook documentation.json
CI / frontend (push) Failing after 1m10s
CI / storybook-a11y (push) Failing after 5m8s
CI / backend (push) Successful in 1m17s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:36:06 +02:00
ehoandClaude Opus 4.8 d4e5a76873 docs: reorganize into project/ + reference/, expand Storybook Foundations
Move working docs (backlog, prd, roadmap) under docs/project/ and durable
docs (architecture ADRs, guides, audits) under docs/reference/; add a
docs/README.md index. Update every path reference in code comments, CLAUDE.md,
READMEs, and the new-ssp skill. Expand the Storybook Foundations curriculum
(Overview, BDD, i18n; rename Layers→Domain-Driven Design) and reorder the sidebar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:33:32 +02:00
ehoandClaude Opus 4.8 55a0a2d166 test: split multi-assertion specs into single-behavior tests
One behavior per test across FE machine/store specs and backend endpoint
tests, so a failure names exactly what broke.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 20:33:25 +02:00
ehoandClaude Opus 4.8 5cae44f163 feat(security): ABAC P2/P3-lite — BIG-nummer redaction, authz audit, guard; clear dev audit
- fix(deps): pin @babel/core ^7.29.7 via overrides → npm audit 0 (dev+prod),
  no --force / no Angular downgrade; README corrected
- feat(brief): field-level PII reveal (PRD-0002 §5c) — CaseContext BIG-nummer
  ships masked; step-up-stubbed (X-Step-Up), audited POST /brief/reveal-bignummer
  unmasks it; drafter-only capability, deny-by-default. Realized on the BIG-nummer
  (no BSN on the wire)
- feat(authz): no-PII AuditAuthz log for reveal attempts + org-admin denials (§8)
- feat(routes): wire capabilityGuard('orgtemplate:edit') onto brief/huisstijl (§6)
- test: backend +5 (Authz + reveal endpoint), FE +3 (adapter boundary, store swap)
- docs: PRD-0002 §5c/§9, WP-18 follow-up, README

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:59:20 +02:00
ehoandClaude Opus 4.8 0edfbba2a9 docs(storybook): render MDX tables + add i18n & testing-strategy docs/skill
- fix: wire remark-gfm into addon-docs so GFM pipe tables in *.mdx render
  (previously raw text in cibg-gaps/layers/atomic-design docs)
- add src/docs/i18n.mdx (Foundations/Internationalization): the $localize
  locale seam + how to test languages without coupling to copy
- add src/docs/testing.mdx (Foundations/Testing strategy): per-layer spec
  matrix, house style, Storybook a11y gate, GREEN gate
- add .claude/skills/test-strategy skill

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:26:34 +02:00
ehoandClaude Opus 4.8 ba32e3dd9f feat(fp): brief v3 — besluit-driven guided drafting
Compose the herregistratie letter from the besluit instead of a library hunt:
the behandelaar picks positief/negatief (+ reden-checkboxes for a negatief) and
the kern's standaardteksten follow the selection live.

Front-end (this increment):
- Kern is recomposed reactively from the besluit selection (new BesluitSelected
  machine msg + composeKern); the "Genereer conceptbrief" button is gone. The
  drafter's free text is preserved across a selection change.
- The editor shows only the editable sections; the locked aanhef/slot render in
  the preview, not the authoring surface. Slot is a case-type template section
  (per templateId), documented as such.
- The panel re-seeds from the letter via inferSelection() — the besluit + redenen
  are read back off the kern's passage blocks, so the selection survives reload
  with no new wire fields (derive, don't store).
- letter-section drops the now-redundant per-section passage picker (besluit owns
  standaardteksten); keeps free-text + block edit/move/remove.

Fix: app-checkbox now falls back to a unique per-instance id when checkboxId is
omitted. The CIBG styled checkbox routes clicks through the label, so the shared
id="undefined" made every reason label toggle the first input — the second
checkbox could never be checked. Verified live (Playwright): each reason toggles
independently.

Backend/seam (brief v3 WIP): besluit/reason passage tags on the wire + seed,
carried through the adapter parse boundary.

Specs updated (besluit, brief.machine) and the affected stories re-pointed at the
new API. FE lint + build + 253 vitest specs green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 18:53:53 +02:00
ehoandClaude Opus 4.8 62ba0b98c4 fix(cibg): address fieldset padding + atomic-design audit cleanup
Fix: address-fields shipped a scoped `fieldset { padding:0; margin:0 }` that
tied on specificity with CIBG's `.form-horizontal fieldset` and, injected later,
won — flattening the grey-box padding on "Mijn gegevens" and the registratie
wizard address step. Removed the override (kept the legend rule).

Audit cleanup (minimise atoms, converge or document near-duplicates):
- delete dead `app-card` (+ story; zero consumers, superseded by app-data-block)
  and the unused `.app-info-box` utility
- review-section raw <h2> -> app-heading; registratie-wizard lone <dl> -> app-data-block
- keep application-link's raw <h3> (vendored `.applications li a h3` chain needs
  the bare element) with an inline justification
- document the deliberate "don't merge these" verdicts in a new Convergence
  decisions table in atomic-design.mdx; record the card removal in cibg-gaps.mdx

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 15:04:02 +02:00
ehoandClaude Opus 4.8 5e36d68f11 feat(fp): WP-27 — brief UX layer (undo/redo, standaardbrief, passage search, diff badges)
CI / storybook-a11y (push) Failing after 4m43s
CI / frontend (push) Failing after 1m15s
CI / backend (push) Successful in 1m24s
CI / codeql (csharp) (push) Failing after 2m51s
CI / e2e (push) Failing after 3h4m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 1m53s
Brief letter-composition UX improvements:
- undo/redo history in the brief store (snapshot stacks, Ctrl/Cmd+Z) + retry-save
- "Standaardbrief invoegen" starter for empty sections; isDefault library passages
  (backend DTO/seed + adapter parse)
- passage-picker client-side search (rich-text textOf helper)
- rejection diff badges on the letter canvas + show/hide changes toggle
  (pure brief-diff domain fns + spec)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 14:36:32 +02:00
ehoandClaude Opus 4.8 950fb5f0b2 fix(cibg): wizard fields render as grey fieldset groups
Wizard steps rendered bare .form-group divs, so CIBG's
".form-horizontal fieldset { background:#f1f5f9; margin-bottom:1.25em }"
never matched and inputs showed on white instead of the grey CIBG surface.

Wrap each logical field group per step in a <fieldset> (intake, herregistratie
and registratie wizards); CIBG then gives every group its grey surface with a
1.25em gap between groups. The shell stays group-agnostic (no outer fieldset,
which would hide the white gaps). address-fields already used a <fieldset>.

Adds intake-wizard.component.spec.ts asserting the buitenland step renders its
groups as separate fieldsets (guards against the wrapping being dropped again).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 13:39:13 +02:00
ehoandClaude Opus 4.8 f6c837f281 feat(fp): WP-26 — admin org-template editor
CI / codeql (javascript-typescript) (push) Failing after 1m19s
CI / frontend (push) Failing after 57s
CI / storybook-a11y (push) Successful in 4m27s
CI / backend (push) Successful in 1m15s
CI / codeql (csharp) (push) Failing after 1m42s
CI / api-client-drift (push) Successful in 1m40s
CI / e2e (push) Failing after 3h11m33s
Edit the letter's org identity in place on the same canvas the drafter composes
on (editableRegions='template'): letterhead/signature/footer become inline
controls, content a read-only sample. Margins (bounded), logo upload (reuses the
shared upload transport + single-upload), version history + rollback, proefbrief,
and publish-with-impact-confirmation. House form-machine idiom
(org-template.machine.ts) + root store with debounced save. Capability-gated
(orgtemplate:edit) with a deny-by-default alert; route /brief/huisstijl.

Backend + generated client were already in place (WP-23). Also fixes a
pre-existing red check:tokens (WP-24 canvas hex fallbacks) and threads the
published logo through to the drafter's canvas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 08:22:55 +02:00
ehoandClaude Sonnet 5 1bb9383344 feat(fp): WP-25 — server-rendered letter HTML preview
Adds LetterHtml.Render, a pure composer mirroring the FE letter canvas'
class vocabulary, behind two ExcludeFromDescription()'d endpoints
(GET /brief/preview, GET /admin/org-template/{subOrgId}/preview).
Auto-resolvable placeholders pull from seed/case data; unresolved
manual ones render as "[NOG IN TE VULLEN: label]". A sent brief
archives its composed HTML (BriefEntity.ArchivedHtml) so a later
org-template republish never changes it. FE gets a hand-written fetch
adapter (text/html, not JSON) and a "Voorbeeld" button that opens the
preview in a new tab.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 12:56:36 +02:00
ehoandClaude Fable 5 c07a33ee3e feat(fp): WP-24 — letter canvas (edit on the letter)
CI / frontend (push) Failing after 59s
CI / storybook-a11y (push) Successful in 4m22s
CI / backend (push) Successful in 1m18s
CI / codeql (csharp) (push) Failing after 1m47s
CI / codeql (javascript-typescript) (push) Failing after 1m20s
CI / api-client-drift (push) Successful in 1m42s
CI / e2e (push) Failing after 3h8m54s
One letter surface for every role: LetterCanvasComponent renders the
org template's letterhead/signature/footer around the case-type
sections, with editableRegions content|template|none. public/letter.css
is the FE⇄BE rendering contract (WP-25 inlines it verbatim).
letter-preview deleted — its read-only rendering absorbed into 'none'
mode. brief.machine.ts byte-identical; orgTemplate parses at the
adapter boundary and lives beside the machine in BriefStore.

Also fixes passage-picker multi-select (checkboxes all shared
id="undefined", so labels only toggled the first box) and keeps the
±page-break marks from drawing through canvas content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:47:51 +02:00
ehoandClaude Fable 5 5a610c10f0 feat(fp): WP-23 — org-template backend + admin role
Second template axis (org identity: letterhead, footer, signature,
margins) server-side: OrgTemplateStore with JSON version history,
publish/rollback, sent-brief version pinning, admin role + capability,
5 admin endpoints, org-logo upload category. FE seam widened only
(Role/Capability unions, interceptor); WP-24/26 consume it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 11:17:05 +02:00
ehoandClaude Sonnet 5 44eb2d2186 chore(deps): update npm packages within declared ranges; reformat for prettier 3.9.4
CI / frontend (push) Successful in 1m46s
CI / storybook-a11y (push) Successful in 4m23s
CI / backend (push) Successful in 1m14s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled
npm update brought every package to the latest version its existing package.json
range allows (Angular tooling 22.0.2/22.0.4 -> 22.0.5, prettier 3.8.4 -> 3.9.4,
typescript-eslint 8.62.0 -> 8.62.1); package.json itself needed no range changes.

Auditing actual deprecation warnings (not just outdated versions) found nothing
further to fix: @angular/platform-browser-dynamic and @angular-devkit/build-angular
are deprecated by Angular but still required peer dependencies of the latest
published @storybook/angular (10.4.6 — peer range still `>=18.0.0 < 22.0.0`,
already why .npmrc sets legacy-peer-deps); jest-process-manager/expect-playwright
are transitive-only through @storybook/test-runner's latest stable (0.24.4). No
newer version of either Storybook package exists yet that drops them. The
remaining npm audit advisory (@babel/core, low severity) is the same
already-documented, deliberately-left issue in README.md (fixing it downgrades
Angular). Left package.json's overrides untouched.

The prettier bump alone changed formatting opinions on files this session didn't
otherwise touch (a stale markdown italics marker, a few object-literal wrap
points) — reformatted everything so `format:check` (part of CI) doesn't regress.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 10:29:36 +02:00
ehoandClaude Sonnet 5 556f2f47bf feat(fp): WP-22 — durable persistence (SQLite/EF Core)
Applications, documents (+ audit log) and the brief move off static in-memory
Dictionaries onto a real SQLite file via EF Core, so demo data survives a
process restart or `docker compose restart api` for the first time. The three
stores (ApplicationStore/DocumentStore/BriefStore) keep their exact public
signatures and static-class shape — no DI, no async ripple into Program.cs's
minimal-API handlers — each method just opens a short-lived AppDbContext via
Db.Create() under the same lock it already had. Opaque nested shapes (a
wizard's draft snapshot, a brief's sections/placeholders/status) are stored as
JSON text columns rather than redesigned into relational tables, matching the
existing "don't interpret it" posture.

Found two things the WP's own text got wrong, corrected in
docs/backlog/WP-22-durable-persistence.md's Deviations section: SeedData never
seeded these three stores (only the read-only BRP/DUO-mimicking GETs, which
stay in-memory) so there's no seed step; and no new docker-compose volume is
needed since the existing bind mount already covers the SQLite file — verified
against this environment's real podman-backed compose stack, not just by
reading the file.

Also: pinned SQLitePCLRaw.bundle_e_sqlite3 to 3.0.3 (EF Core Sqlite's own
transitive default bundles a pre-3.50.2 SQLite with a known high-severity
memory-corruption advisory); found and fixed a real xUnit test race where
concurrent test-class hosts stomped a shared static connection-string field,
fixed by disabling cross-class test parallelization rather than adding DI the
stores don't otherwise need.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-05 10:19:23 +02:00
ehoandClaude Sonnet 5 40dbcb2606 feat(fp): WP-21 — resilience seams (correlation-id, idempotency, retry)
Correlation id becomes real ASP.NET Core middleware instead of a per-endpoint
read: every request gets one (client-supplied or generated), it's echoed as
an X-Correlation-Id response header, and pushed into the logging scope so
every log line for that request carries it — not just the Submit helper's,
verified against LogBrief which never threads it explicitly.

Idempotency-Key moves from per-HTTP-attempt (defeating its own purpose) to
per-logical-submit: runSubmit mints one key and threads it through a small
bridge (withIdempotencyKey/currentIdempotencyKey) since the NSwag-generated
client has no per-call header hook. Backend gains an IdempotencyStore that
short-circuits a replayed key to the first call's result instead of minting
a second reference — scoped to the Submit-helper endpoints per the WP's own
decision.

GET requests now retry transient failures (rxjs retry({count:2, delay:500}));
writes never auto-retry. Proven with a fake-HttpClient spec
(api-client.provider.spec.ts) rather than a manual network-tab check — the
WP's suggested `?scenario=error` check turned out not to exercise a real
network call at all (the interceptor throws before calling next()), so the
automated test is the actual proof.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 20:03:41 +02:00
ehoandClaude Sonnet 5 e276629107 feat(fp): WP-20 — second locale proof (nl/en build seam)
angular.json gains an i18n block (sourceLocale nl, en translation file) and
an `en` build/serve configuration with i18nMissingTranslation: "error" so a
new $localize string without an English unit fails the build, not silently
falls back. CI now runs `ng build --localize` to build both locales every
run. Verified end-to-end, not just "the build succeeded": the nl bundle
ships "Inloggen met DigiD", the en bundle ships "Log in with DigiD".

Incidental: prettier/compodoc regen noise in docs/wcag-checklist.md,
src/docs/a11y.mdx, documentation.json from the same working session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:16:11 +02:00
ehoandClaude Sonnet 5 26c2c5acd0 feat(fp): WP-19 — Playwright e2e smoke against the real FE+backend
Adds a happy-path spec (login → dashboard → registratie wizard, including
a real identity-document upload → real submit) and a degraded-path spec
(?scenario=error → <app-async> error slot → retry), both driving the real
app against the real .NET backend, plus a CI job that boots both.

Writing the retry spec surfaced a real bug: AsyncComponent's retry() only
reloads a [resource]-fed instance, so every real page (all [data]-fed via
a store's RemoteData) had a silently no-op retry button. Added a
retryClicked output and wired it on the dashboard's two async blocks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 10:13:40 +02:00
ehoandClaude Sonnet 5 e272869f00 feat(fp): WP-17 — app-level a11y: route focus, template lint, WCAG checklist
Adds route-change focus management (new page's h1, afterNextRender) plus
scroll-position restoration wired once in app.config.ts; angular-eslint's
templateAccessibility bundle linting every inline template via
processInlineTemplates (verified firing with a planted violation, one real
hit fixed in rich-text-editor); docs/wcag-checklist.md and Foundations/
Accessibility MDX tying the four a11y layers (axe, lint, play tests,
manual checklist) together. The checklist pass already earned its keep —
it found a real 320px overflow in aanvraag-block's warning alert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 08:26:01 +02:00
ehoandClaude Sonnet 5 f3de30b72c feat(fp): WP-16 — component a11y: description wiring + alert role
Wires text-input's aria-describedby to the form-field description div
(the BSN hint was rendered but never announced), pins desc-before-error
ordering, and switches alert to role=alert for errors vs role=status
for info/ok/warning. Composition contract enforced by story play tests
(form-field+text-input, alert per variant) run in the WP-01 CI gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 08:15:24 +02:00
ehoandClaude Sonnet 5 85c805b8bd docs(backlog): backfill WP-15 commit hash
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:38:07 +02:00
ehoandClaude Sonnet 5 0cfb01f12c feat(fp): WP-15 — missing stories: shell + brief components
Add the 7 stories CLAUDE.md's testing rule ("UI is exercised via Storybook
stories") was missing: shared/layout/shell (Design System/Templates/Shell)
and all six previously-unstoried brief components (passage-picker,
rejection-comments, diagnostics-panel, letter-block, letter-preview,
letter-section — Domein/Brief/*), each with a default state plus the
meaningful variants (locked/editable, findings/clean, show/entry, etc).
Every *.component.ts in the repo now has a co-located story; *.page.ts
files stay unstoried, matching the existing norm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:37:58 +02:00
ehoandClaude Sonnet 5 ac1f0b6aeb docs(backlog): backfill WP-14 commit hash
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:32:26 +02:00
ehoandClaude Sonnet 5 8b19fad558 feat(fp): WP-14 — Storybook taxonomy reorg + Layers MDX
Retitle all 49 stories into a sidebar that makes the DDD seam visible:
Foundations (curriculum) -> Design System (Atoms/Molecules/Organisms/
Templates/Devtools, everything in shared/ui + shared/layout) -> Domein
(Registratie/Herregistratie/Auth/Brief, everything in a context's ui/).
Pin the order via storySort. Add layers.mdx explaining the split and
linking the enforcing eslint rules; document the story-title convention
in CLAUDE.md. Fix a stale "status banner" reference in atomic-design.mdx
left over from WP-13's upload-status-banner deletion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:32:19 +02:00
ehoandClaude Sonnet 5 cbf697b8fa docs(backlog): backfill WP-10/WP-13 commit hashes
CI / frontend (push) Failing after 54s
CI / storybook-a11y (push) Successful in 4m2s
CI / backend (push) Successful in 1m2s
CI / codeql (csharp) (push) Failing after 1m40s
CI / codeql (javascript-typescript) (push) Failing after 1m23s
CI / api-client-drift (push) Successful in 1m30s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:42:26 +02:00
ehoandClaude Sonnet 5 9d58f597ea feat(fp): WP-13 — CIBG-gap register + hygiene + MDX
Mark every hand-rolled shared/ui surface with a `// CIBG-GAP EXTENSION:`
comment + `cibgGap` story parameter (skeleton, spinner, rich-text-editor,
wizard-shell's error summary, application-link's non-navigating row,
debug-state, status-badge, card, placeholder-chip) so deviations from the
CIBG design system are auditable. Add the register MDX
(Foundations/CIBG Gap Register), cross-linked from ADR-0003. Delete the
near-identity upload-status-banner wrapper; its one consumer now uses
<app-alert> directly (a story added to keep the info-banner state covered).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:42:13 +02:00
ehoandClaude Sonnet 5 69880efd38 feat(fp): WP-10 — CIBG button fidelity
Fix button atom's dead .btn-outline-primary → .btn-secondary; add 'ghost'
variant (.btn-ghost, CIBG-documented). RTE toolbar drops invented
.btn-outline-secondary/.btn-sm for .btn-ghost. file-input already used the
correct vendored .btn-upload pattern from the earlier CIBG UI fidelity
pass — no change needed there (documented as a deviation).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 22:34:28 +02:00
eho 8078c499cb feat(fp): WP-09 — pure-logic closure: dates + missing command specs
Consolidate four hand-rolled nl-NL date formatters (tasks.ts, aanvraag-
block, letter-preview, aanvraag-view -- one more than the WP found) into
one shared/kernel/datum.ts::formatDatumNl, spec-pinned and empty-safe.
Add the two missing command specs CLAUDE.md's testing rule calls for:
draft-sync.spec.ts (debounce coalescing + trailing-call + submit Result
shape, via fake timers) and submit-change-request.spec.ts. Remove the
unused RemoteData.map3 (updating the three docs that mentioned it); the
variant input on confirmation.component.ts was already gone. Documents
both stale-WP-text corrections in the backlog file.

This closes out backlog Phase 1 (FP/DDD core, WP-05..09).
2026-07-03 22:02:50 +02:00
eho 0d623f90e8 feat(fp): WP-08 — one store idiom + machine naming + TEA MDX
Rename change-request.machine.ts's bare State/Msg to ChangeRequestState/
ChangeRequestMsg (the last machine not context-prefixed), document the
createStore-is-the-idiom + naming convention in CLAUDE.md §3, and add the
Foundations/State Machines (TEA) curriculum page. The wizard pages already
wired createStore (confirmed by reading each and by git log) -- the WP's
"hand-wired signal(model)" premise was stale; recorded as a deviation.
2026-07-03 21:50:53 +02:00
eho e3cd908f4f feat(fp): WP-07 — brief on the shared idioms + RemoteData MDX
Collapse brief.store's busy signal + nullable lastError into one Idle |
Busy | Failed union (saveState gets matching tag-object style), and route
brief.page's load through RemoteData + <app-async> instead of a hand-rolled
@switch, via a BriefStore.remoteData projection of the machine's existing
loading/failed tags -- the machine keeps owning the letter's own status
lifecycle untouched. New brief.store.spec.ts covers the Busy->Idle/Failed
transitions; new Foundations/RemoteData & Async MDX page documents the
pattern and the WP-06 typed-loaded-slot fallback. Deviation from the
original plan recorded in the WP file.
2026-07-03 21:39:29 +02:00
eho 199cbe1f8c feat(fp): WP-06 — kill $any() in templates (18x)
Make AsyncLoadedDirective generic with a static ngTemplateContextGuard for
AsyncComponent's own internal typing. That can't propagate to consumer
`<ng-template appAsyncLoaded let-p>` sites though -- Angular only infers a
structural directive's type parameter from an input bound on that same
node, not from a sibling input on the parent component -- so the ~9
root-cause consumers (dashboard, registration-detail, aanvraag-detail,
registratie-wizard) instead unwrap the RemoteData Success value via a
typed computed() and narrow it locally with `@if (x(); as p)`. The
remaining union-narrowing casts (registration-summary, showcase concepts
page) are replaced with a stable @let binding and a direct resource read,
respectively. Documented as a deviation in WP-06's backlog file.
2026-07-03 21:27:01 +02:00
eho 34d34512b3 feat(fp): WP-05 — parse-don't-validate closure + MDX
Close the three remaining unvalidated `as <DomainType>` casts at the wire
boundary (intake-policy, big-register aantekening type, brief passage scope),
each replaced by a Result-returning parser with a rejection-case spec, plus
the Foundations/Parse, don't validate curriculum page.
2026-07-03 21:02:15 +02:00
ehoandClaude Sonnet 5 5d6a78d4ec docs(backlog): record WP-18 commit hash
CI / frontend (push) Successful in 1m22s
CI / backend (push) Successful in 1m3s
CI / api-client-drift (push) Successful in 1m36s
CI / storybook-a11y (push) Successful in 4m4s
CI / codeql (csharp) (push) Failing after 1m45s
CI / codeql (javascript-typescript) (push) Failing after 1m25s
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:32:03 +02:00
ehoandClaude Sonnet 5 7ec13d8b59 feat(brief): WP-18 — ABAC capability spine (PRD-0002 phase P1)
Replace the FE-computed authorization anti-pattern in BriefStore.editable
(derived from the unverified X-Role header) with server-computed decision
flags, mirroring the existing HerregistratieDecisionsDto pattern:

- Backend: Authz.cs is the single authorization helper — the SAME check
  (Authz.CanActOn) both gates BriefStore.Review's mutations and computes
  the BriefDecisionsDto flags shipped on every brief response, so emit
  and enforce can never drift. New GET /me returns coarse, role-derived
  capabilities (PRD-0002 SS6).
- Every brief endpoint (including send, previously ungated on HttpContext)
  now returns a fresh BriefViewDto so decisions never go stale after a
  mutation.
- FE: brief.store.ts reads canEdit/canApprove/canReject/canSend off the
  loaded decisions instead of computing them from currentRole(); the
  brief.machine carries decisions through every status transition.
- New shared/domain/capability.ts + shared/application/access.store.ts +
  shared/infrastructure/me.adapter.ts: the general capability-spine
  infrastructure (AccessStore.can(), capabilityGuard) for future routes.

Deviates from the original WP-18 draft by NOT renaming auth/domain's
Session to a Principal union — ADR-0002 explicitly defers that refactor
until a second actor exists, and the brief workflow's drafter/approver
identity turned out to be a separate axis from the SSP login session
entirely. See docs/backlog/WP-18-abac-capability-spine.md for the full
as-built record.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:31:53 +02:00
ehoandClaude Sonnet 5 cbb8ae548c docs(backlog): add WP-18..22 (productie-volwassenheid phase)
CI / storybook-a11y (push) Successful in 4m12s
CI / backend (push) Successful in 1m6s
CI / api-client-drift (push) Successful in 1m37s
CI / frontend (push) Successful in 1m31s
CI / codeql (csharp) (push) Failing after 1m51s
CI / codeql (javascript-typescript) (push) Failing after 1m24s
Gap analysis found the POC's designed-but-unbuilt strategic gaps: ABAC
authorization (ADR-0002/PRD-0002 phase P1), no e2e coverage, unproven
i18n second-locale seam, thin resilience seams (correlation-id,
idempotency, retry), and in-memory-only persistence. Each WP is grounded
in the current code (file paths + line numbers), not just the analysis.

Also corrects PRD-0001's stale 'Proposed' status header — the Mijn
aanvragen vertical is fully built.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 20:07:05 +02:00
ehoandClaude Opus 4.8 bf920696ac ci: harden workflow + add security scanning and format gates
CI / storybook-a11y (push) Successful in 4m3s
CI / backend (push) Successful in 1m1s
CI / codeql (csharp) (push) Failing after 39m18s
CI / codeql (javascript-typescript) (push) Failing after 1m22s
CI / frontend (push) Successful in 1m25s
CI / api-client-drift (push) Successful in 1m34s
- permissions: contents:read (least privilege), concurrency cancel,
  scope push to main+tags (was: every branch, double-running with PRs),
  per-job timeout-minutes.
- security: npm audit --omit=dev, CodeQL SAST (TS + C#), Dependabot
  (npm/nuget/actions).
- format: npm run format:check + dotnet format --verify-no-changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00
ehoandClaude Opus 4.8 1137f59f7b style: format backend with dotnet format
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 13:39:31 +02:00