Files
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

40 KiB
Raw Permalink Blame History

Scope: ADR-0001…0006 (all six), checked against apps/ssp, apps/behandelportal, libs/shared, libs/beheer, backend/src/BigRegister.Api, backend/tests, e2e

Status: complete

Last updated: 2026-08-26

Depends on: 00-baseline.md

---

06 — ADR conformance

Every claim below was verified by reading the code, not by trusting the ADR text or CLAUDE.md. Where the two disagree with each other and with reality, that is flagged as an ADR-fix, per CLAUDE.md's own "the docs win — update this file" rule.

Headline. The architecture is in good shape. All six ADRs are substantially conformed to in their decisions; there is no case where the codebase has quietly adopted a different architecture. What has decayed is the ADRs' descriptive prose — four of the six describe a repo layout, a set of files, or an out-of-scope list that WP-49…WP-75 has since overtaken. Five of the eleven findings are therefore ADR-fixes.

On BL-002 (the auth duplication), my verdict up front, because the brief asked for an argument rather than an assertion: ADR-0002's non-sharing rule needs no amendment and should not be relaxed. The prediction has not failed; it is unexecuted. See ADR-C-004/ADR-C-006 for the reasoning and for what should actually be filed instead.


ADR-0001 — BFF-lite endpoints + decision DTOs (Accepted)

Core decision: conformed. The two policy shapes the ADR prescribes are both live and correctly applied.

  • Decision flag — verified server-computed and rendered, never recomputed: apps/ssp/src/app/brief/application/brief.store.ts:103-108 derives canEdit/ canApprove/canReject/canSend/canRevealBigNummer purely from BriefState.loaded.decisions, with an explicit comment at line 31 ("this store never computes them itself"). Same shape in apps/behandelportal/src/app/behandeling/infrastructure/beoordeling.adapter.ts:87-97 (decisions.canBesluiten, rejected at the parse boundary if absent).
  • Config valueapps/ssp/src/app/herregistratie/domain/intake.machine.ts:52 takes scholingThreshold as a parameter; SCHOLING_THRESHOLD_DEFAULT (line 43) survives only as the offline fallback the ADR sanctions, wired through application/intake-policy.store.ts:18-19. No hardcoded 1000 is used as authority.
  • Parse boundary — 30 parse* functions (baseline §7). ADR-0001's own "out of scope" item "Runtime DTO validation on every endpoint (only the dashboard view has it)" is substantially discharged.
  • libs/shared/src/application/remote-data.ts + <app-async>, and dep:check's ui-not-infrastructure rule at 0 violations (baseline §6), keep the "infrastructure is the only network layer" clause enforced — with one carve-out, ADR-C-002.

ADR-C-001 — ADR-0001's worked example describes a POC that no longer exists

  • Type: ADR-fix
  • ADR cited: ADR-0001, §"Worked example in this POC" — opening sentence "This POC has no real backend (static mock JSON + fake submit timers), so the 'BFF output' is a static file"; §"Out of scope here", bullets 1 and 4.
  • Evidence (verified):
    • There is a real backend: backend/src/BigRegister.Api/Program.cs (940 lines, 48 endpoint mappings — baseline §2/BL-003).
    • Every artifact the worked example names by path is gone. find returns nothing for public/mock/dashboard-view.json, public/mock/intake-policy.json, or src/app/herregistratie/contracts/intake-policy.dto.ts. apps/ssp/public/mock/ does not exist. The surviving contract file moved to apps/ssp/src/app/registratie/contracts/dashboard-view.dto.ts (WP-67's src/apps/ssp/src/ move), so §A's three cited paths are all wrong.
    • Out-of-scope bullet 1 ("runtime DTO validation on every endpoint — only the dashboard view has it") is stale: 30 parse* boundaries exist. Out-of-scope bullet 4 ("Real OpenAPI/TypeSpec codegen toolchain") is stale: npm run gen:api generates libs/shared/src/infrastructure/api-client.ts (2372 lines, NSwag) and CI drift-checks it.
  • Baseline citation: baseline §2 (Program.cs 940 lines), §7 pattern inventory (parse* trust boundaries = 30; excluded-as-generated api-client 2372 lines), BL-003.
  • Proposed resolution: rewrite §"Worked example" against the shipped system and prune the discharged out-of-scope bullets. No code changes.
  • What the amended ADR should say: the worked example should read as "implemented against backend/src/BigRegister.Api", cite apps/ssp/src/app/registratie/{contracts/dashboard-view.dto.ts,infrastructure/dashboard-view.adapter.ts} and GET /api/v1/dashboard-view / GET /api/v1/intake/policy as the endpoints, and reduce §"Out of scope" to the two items still genuinely open (the BigProfileStore optimistic-update race, and session persistence / multi-tab sync).
  • Blocked code tickets: none. This is purely descriptive drift; the decision is intact.
  • Effort: S

ADR-C-002 — libs/shared/src/upload/ does network outside infrastructure/

  • Type: code-violates-ADR
  • ADR cited: ADR-0001, §Decision — the DTO/adapter seam; operationalised in CLAUDE.md §4 as "infrastructure/ is the only layer that touches the network — the anti-corruption boundary".
  • Evidence (verified): libs/shared/src/upload/upload.adapter.ts injects ApiClient (line 3 import, line 56 inject(ApiClient)) and opens a raw XMLHttpRequest at line 118 — i.e. it is a genuine network adapter — yet sits in a top-level upload/ folder, not under libs/shared/src/infrastructure/. Its sibling upload.machine.ts is an Elm-style reducer sitting outside any domain/ folder (the only one of 9 machines to do so). The exception is hard-coded into the enforcement itself: .dependency-cruiser.base.js:105 reads from: { pathNot: '/infrastructure/|^libs/shared/src/upload/' } — the rule is written around the violation rather than the violation being fixed, which is why baseline §6 reports 0 violations despite this.
  • Baseline citation: BL-010 (verbatim: the exception "is already encoded rather than resolved"); baseline §7 FE pattern inventory ("Infrastructure adapters (read side): 20 +1 outside an infrastructure/ folder"; "Elm-style machines: 9 — 8 under a domain/ folder; outlier libs/shared/src/upload/upload.machine.ts").
  • Proposed resolution: move upload.adapter.tslibs/shared/src/infrastructure/, upload.machine.ts (+ its spec) → libs/shared/src/domain/, and upload-controller.ts/upload-shell.service.tslibs/shared/src/application/. Then delete the ^libs/shared/src/upload/ carve-out from .dependency-cruiser.base.js:105 — that deletion is the acceptance criterion, since it is what proves the exception is resolved rather than relocated. Note the side benefit: libs/shared/src/domain currently has 0% spec reach across 3 files (baseline §3b), and this moves a well-specced machine into it.
  • Effort: M (mechanical move + import updates across 30 dependents — libs/shared/upload has Ca 30, baseline §6 — plus the depcruise rule edit)

ADR-C-003 — contracts/ vs the generated client: ADR-0001 and CLAUDE.md §4 no longer agree with the code

  • Type: ADR-fix
  • ADR cited: ADR-0001, §"Why DTOs decouple rather than couple" — "Manage it with one source of truth (OpenAPI or TypeSpec) that generates types for both sides" — read against CLAUDE.md §4's flat rule "DTO lives in contracts/".
  • Evidence (verified): only 4 hand-written contracts/ DTO files exist (apps/ssp/src/app/registratie/contracts/{brp-address,dashboard-view,duo-diplomas}.dto.ts, libs/beheer/src/contracts/stamdata.dto.ts), against 20 infrastructure adapters, 19 of which import types from @shared/infrastructure/api-client directly. Crucially, the surviving hand-written contract documents its own obsolescence: dashboard-view.dto.ts:10-11 says "In production these types are GENERATED from the OpenAPI/TypeSpec spec (one source of truth for both sides)". So the code has reached ADR-0001's stated target state, and the hand-written contracts/ files are the pre-codegen scaffolding — but CLAUDE.md §4 still states the pre-codegen rule as the standing law, which will mislead the next feature author.
  • Baseline citation: baseline §7 FE pattern inventory — "contracts/ DTO files: 4 — most adapters consume NSwag-generated types directly instead"; agent-brief note "Only 4 contracts/ DTO files exist".
  • Proposed resolution: amend ADR-0001, then correct CLAUDE.md §4 to match (CLAUDE.md's own precedence rule requires this direction).
  • What the amended ADR should say: add a short §"Where the contract lives, after codegen": the generated client (libs/shared/src/infrastructure/api-client.ts, regenerated by npm run gen:api, drift-checked in CI) is the wire contract and the single source of truth; a hand-written contracts/*.dto.ts is warranted only where codegen does not reach — a hand-rolled fetch/XHR endpoint, or a shape the generator types too loosely — and in either case the hand-written file must still import nothing. The parse* trust boundary in infrastructure/ is unchanged and still mandatory regardless of where the type came from: a generated type is a compile-time claim about the wire, not a runtime guarantee. Then decide explicitly whether the 4 survivors stay (they are more precise than the generated shapes) or are retired — and record which.
  • Blocked code tickets: any ticket that would either (a) delete the 4 remaining contracts/ files or (b) add new hand-written DTOs for already-generated endpoints must wait for this amendment; today CLAUDE.md §4 can be cited to justify both directions.
  • Effort: S (ADR + CLAUDE.md edit; the follow-on code decision is separately sized)

ADR-0002 — User groups as actors, not bounded contexts (Proposed, amended WP-67)

This is the ADR with the most divergence, and the one the baseline pointed me at. Three findings. The core modelling decision — contexts drawn by capability, not by who logs in — is conformed to: there is no zorgverlener/ or behandelaar/ folder anywhere, the contexts are registratie/herregistratie/brief/behandeling/beheer (capability names), and both apps integrate through one backend aggregate via decision DTOs.

ADR-C-004 — the Principal union never landed, although actor #2 did

  • Type: code-violates-ADR
  • ADR cited: ADR-0002 §3 ("Separate identity from authorization") — "Model the principal as a discriminated union … This replaces the flat Session the day a second actor arrives"; §Consequences — "The one concrete FE change when actor #2 lands is Session → Principal in the auth context"; §"Out of scope" — deferred "until a second actor is actually introduced". Actor #2 was introduced in WP-61 and consolidated in WP-67. The deferral condition has been met and the change was not made.
  • Evidence (verified):
    • grep -rn "Principal" apps libs --include=*.ts returns exactly one hit, and it is a comment: libs/shared/src/infrastructure/role.ts:8. The type does not exist in the frontend.
    • apps/behandelportal/src/app/auth/domain/session.ts is byte-identical to the SSP's and still reads interface Session { readonly bsn: string; readonly naam: string } — a Behandelaar carrying a BSN, which §3 names as the precise thing the union exists to make unrepresentable.
    • The backoffice login is literally the citizen login. apps/behandelportal/src/app/auth/ui/login.page.ts:31 is async login(bsn: string), renders intro="Log in op uw persoonlijke BIG-register omgeving.", and calls SessionStore.login(bsn)auth/infrastructure/digid.adapter.ts:14, which returns ok({ bsn: r.value, naam: 'Dr. A. (Anna) de Vries' }). A behandelaar logs into the backoffice as a zorgverlener, by DigiD, with a citizen's name in the header.
    • The medewerker identity that does exist bypasses the auth model entirely: apps/behandelportal/src/app/auth/infrastructure/medewerker.interceptor.ts:14-19 stamps X-Medewerker/X-Rollen onto every /api/v1/ request. It never touches Session.
  • Baseline citation: BL-002 (ssp/auth 211/211 significant lines duplicated; session.store.ts 39 windows, login-form.component.ts 35, login.page.ts 23) — the 100% figure is the measurement of this finding: the files are identical because the modelling change that would differentiate them was skipped.
  • Proposed resolution: land Session → Principal as ADR-0002 §3 specifies. In apps/behandelportal: replace Session with the medewerker variant, replace DigidAdapter with a MedewerkerAdapter that resolves MEDEWERKER_ID + currentRollen() (the values medewerker.ts already holds) into a Principal, and make login.page.ts an SSO-stand-in entry rather than a BSN form. In apps/ssp: the zorgverlener variant. This makes the two auth contexts genuinely differ — which is what ADR-0002 §3 asserted would happen and is the honest resolution of BL-002.
  • Effort: M

ADR-C-005 — ADR-0002 is still Proposed after two apps shipped against it

  • Type: ADR-fix
  • ADR cited: ADR-0002 header, Status: Proposed · Date: 2026-07-01.
  • Evidence (verified): apps/behandelportal exists with 29 source files and 3 contexts (baseline §2); the ADR has been amended once in-document (§"Amendment (WP-67, 2026-08-01)"); its structural rulings are enforced in CI today — .dependency-cruiser.<app>.js's <app>-no-other-app and shared-no-beheer rules run at severity: error with 0 violations. An architectural decision that CI enforces is not "Proposed". The other five ADRs are all Accepted, so this is an inconsistency in the ADR set itself, not a deliberate signal.
  • Baseline citation: baseline §7 "ADRs on record" — "0002 … (Proposed, amended WP-67)"; baseline §6 (11 severity: error rules, 0 violations, 223 modules cruised); baseline §2 (apps/behandelportal: 29 src files, 1 309 lines).
  • Proposed resolution: promote to Accepted, dated to WP-67.
  • What the amended ADR should say: Status: Accepted · Date: 2026-07-01 · Amended 2026-08-01 (WP-67). Also update §"Out of scope here", which still lists "Building the Behandeling backoffice application" and "The backend aanvraag status lifecycle + authorization endpoints/DTOs" as unbuilt — both shipped (WP-61…67; AanvraagStatusTag, GET /me capabilities, Domain/Authz.cs). The one bullet that stays is real authentication. Leave the Session → Principal bullet in scope but re-word it from "deferred until a second actor is introduced" to a stated debt — it is ADR-C-004.
  • Blocked code tickets: none strictly, but agent 08 should surface this before ADR-C-004, because ADR-C-004's justification is "the ADR says to do this" and a Proposed ADR is weak grounds for a refactor ticket.
  • Effort: S

ADR-C-006 — extract the actor-agnostic route guards to libs/shared (the part of BL-002 that will never diverge)

  • Type: code-violates-ADR — but note carefully: it violates CLAUDE.md §2's "composition over duplication" and the DRY intent, not ADR-0002 §3. ADR-0002 §3 is about identity and login flow. A route guard is neither.
  • ADR cited: ADR-0002 §3 — the scope of the non-sharing decision is Principal and the login flow ("the two groups authenticate differently"); §Consequences names auth.guard.ts and session.store.ts only as the seams that localise the change, not as things that must be duplicated. Read with CLAUDE.md §1's rule that a genuinely cross-app concern belongs in libs/shared.
  • Evidence (verified): diff -ru apps/ssp/src/app/auth apps/behandelportal/src/app/auth reports no content differences at all — 9 of 11 files are byte-identical; the only delta is two additional files in behandelportal (medewerker.ts, medewerker.interceptor.ts). Within those 9, auth.guard.ts is entirely actor-agnostic: authGuard (lines 8-12) reads only SessionStore.isAuthenticated() and Router; capabilityGuard (lines 25-33) adds only AccessStore.can(capability) and whenReady() — and AccessStore already lives in libs/shared/src/application. Both apps' routes redirect to the same /login and /dashboard paths (apps/behandelportal/src/app/app.routes.ts:10,17). Both app configs already register { provide: SESSION_PORT, useExisting: SessionStore } (apps/behandelportal/src/app/app.config.ts:65), so the seam for a shared guard exists today.
  • Baseline citation: BL-002 top clone pairs — auth.guard.spec.ts 36 windows and auth.guard.ts 21 windows, i.e. 57 of the 211 duplicated lines, the single largest block after session.store.ts.
  • Proposed resolution: move authGuard/capabilityGuard + auth.guard.spec.ts to libs/shared/src/application/ (or a libs/shared/src/routing/), injecting SESSION_PORT instead of the app-local SessionStore. One small widening is needed: libs/shared/src/application/session.port.ts:9-12 currently exposes only session: Signal<{naam: string} | null> and logout() — add readonly isAuthenticated: Signal<boolean> (or have the guard derive it from session() !== null, which both SessionStores already do at session.store.ts:40). Each app keeps a two-line re-export at @auth/auth.guard so its app.routes.ts is untouched.
  • Explicitly NOT proposed, and why. No ticket to merge session.store.ts, session.ts, digid.adapter.ts, login-form.component.ts or login.page.ts, and no ADR-fix relaxing ADR-0002 §3. The brief invited me to treat BL-002 as a prediction the code failed to bear out. It is not. Those five files are identical because ADR-C-004 was never executed — the divergence the ADR predicted has in fact already arrived, it just arrived through an orthogonal side door (medewerkerInterceptor, a dev-only HTTP header stamp) instead of through the Principal seam the ADR designated. Merging them now would cement a citizen DigiD/BSN login as the backoffice's shared login, which is the one outcome ADR-0002 §3 was written to prevent. The correct sequencing is ADR-C-005 (accept the ADR) → ADR-C-004 (land Principal) → re-measure. My expectation is that post-ADR-C-004 the residual ssp/authbhp/auth duplication drops from 211 lines to under 40 on its own. If ADR-C-004 is still unstarted at the next backlog cycle, that is the point at which the ADR-fix conversation becomes legitimate — not now.
  • Effort: S

Observation for agent 07 (BIO2), not a ticket here

medewerkerInterceptor is registered only inside isDevMode() (apps/behandelportal/src/app/app.config.ts:57-63). In a production build the backoffice therefore sends no X-Medewerker/X-Rollen at all, and StubIdentityProvider falls through to its zorgverlener default. ADR-0002 §3's "authorization enforced at the backend boundary" holds structurally, but the behandelportal's identity has no non-dev path. The ADR lists real employee SSO as out of scope, so this is not an ADR conformance defect — flagging it because it is the kind of thing a compliance pass should see stated, and because ADR-C-004 is the natural place to close it.


ADR-0003 — CIBG Huisstijl (Bootstrap 5.2) as the design system (Accepted)

Core decision: conformed. All five decision points hold. The package is vendored at public/cibg-huisstijl/ (css, fonts, icons, images present, licensed RO/Rijks text fonts absent per point 5); the token bridge is intact at libs/shared/styles.scss; atoms emit Bootstrap classes with their input() APIs preserved; check:tokens (scripts/check-tokens.sh) runs in npm run ci; the gap register exists at libs/shared/docs/cibg-gaps.mdx with 9 // CIBG-GAP EXTENSION: markers in code.

ADR-C-007 — ADR-0003's file paths and its app-alert example are both stale

  • Type: ADR-fix
  • ADR cited: ADR-0003 §Decision point 1 (src/index.html), point 2 (src/styles.scss), point 4 ("CIBG omits Bootstrap's .alert and .navbar, so app-alert is a small token-styled surface"), §Consequences (.storybook/, src/docs/cibg-gaps.mdx).
  • Evidence (verified):
    • Every path moved in WP-67: src/styles.scsslibs/shared/styles.scss; src/index.htmlapps/ssp/src/index.html and apps/behandelportal/src/index.html (two now, not one); .storybook/.storybook-ssp/ and .storybook-behandelportal/; src/docs/cibg-gaps.mdxlibs/shared/docs/cibg-gaps.mdx.
    • Point 4's app-alert claim is factually wrong about the current code. libs/shared/src/ui/alert/alert.component.ts:13-16 documents itself as a "Thin wrapper over the vendored .feedback feedback-* classes: the design system owns surface + icon", and the template (lines 31-37) binds .feedback-info/.feedback-success/ .feedback-warning/.feedback-error. Its only local CSS is a 3-line flex fix. It is not hand-rolled and carries no CIBG-GAP marker — correctly, since it is not a gap.
    • CLAUDE.md §2 repeats the same stale claim verbatim: "(Where CIBG lacks a class — e.g. alert — the atom is a small hand-rolled surface built from the token bridge; see ADR-0003.)" Both documents must be corrected, ADR first.
  • Baseline citation: baseline §2 (the monorepo layout the paths must be rewritten against: apps/ssp, apps/behandelportal, libs/shared, libs/beheer); baseline §7 config-seam tokens (HEADER_NAV_ITEMS/HEADER_ADMIN_LINKS/DEBUG_PANEL) — the WP-67 two-app split these paths belong to.
  • Proposed resolution: repoint all five paths; replace the .alert example in point 4.
  • What the amended ADR should say: point 4 should keep the principle ("hand-roll what CIBG's build drops, and mark it") but pick a live example — skeleton and spinner are the cleanest (both in the register, both genuinely absent from the vendored build) — and note that .alert was subsequently resolved onto the vendored .feedback classes, so it is no longer a gap. CLAUDE.md §2's parenthetical must be corrected in the same diff.
  • Blocked code tickets: none.
  • Effort: S

ADR-C-008 — the CIBG gap register is one row behind the markers in code

  • Type: code-violates-ADR (the artifact the ADR mandates is incomplete; the fix is a one-row doc edit, not a code change)
  • ADR cited: ADR-0003 §Consequences, final bullet — "Hand-rolled components (point 4) are tracked in the CIBG gap register … every deviation from the design system carries a // CIBG-GAP EXTENSION: marker so it's auditable rather than silently drifting."
  • Evidence (verified): 9 files carry a CIBG-GAP marker (debug-state, language-switcher, wizard-shell, application-link, placeholder-chip, rich-text-editor, skeleton, spinner, status-badge); the register table at libs/shared/docs/cibg-gaps.mdx:26-33 has 8 rows. libs/shared/src/layout/language-switcher/language-switcher.component.ts:7-9 carries a full, well-formed marker ("Taal instellen" … no vendored Huisstijl class ships for it … See cibg-gaps.mdx) and has no corresponding row. The register's own §"Keeping this register honest" concedes there is no automated diff and instructs "If markers and this table drift, trust the code and fix the table" — so this is exactly the drift it predicted, caught by review as designed.
  • Baseline citation: baseline §2 (libs/shared 86 src files / 5 194 lines — the surface the register must cover); baseline §6 (libs/shared/src/layout is a cruised module with Ca 22).
  • Proposed resolution: add the language-switcher row (CIBG concept: "Taal instellen"; reason: no vendored class in this build). Optionally add the CI script the register declines — a ~10-line grep -l CIBG-GAP | diff in scripts/ folded into check:tokens would make the drift impossible to reintroduce. I would file the row as the ticket and the script as an explicitly optional second step, matching the register's own proportionality argument.
  • Effort: S

ADR-0004 — Stamdata as code (Accepted)

Core decision: conformed. backend/src/BigRegister.Api/Stamdata/ holds 15 files — typed C# (Beroep.cs, PolicyQuestions.cs, Professions.cs, StamdataCatalog.cs, StamdataTable.cs, StamdataFile.cs) plus the checked-in JSON data-files (beroepen.json, documentconfidentialiteit.json, opleidingen.json, professions.json, specialismen.json) the WP-29 follow-on introduced. Coverage is 96.8% line (baseline §3c). The beheer/stamdata editor is read-only-plus-download, not a write path, exactly as the ADR's own WP-29 note states. There is no runtime write endpoint for any stamdata table.

ADR-C-009 — feature flags are a second runtime-editable exception the ADR does not acknowledge

  • Type: ADR-fix
  • ADR cited: ADR-0004 §Decision — "Never a production database, never runtime-editable" — and §"The deliberate exception: org-templates", which names one exception in the singular and justifies it narrowly ("specific to one sub-organization's identity").
  • Evidence (verified): backend/src/BigRegister.Api/Data/FeatureFlagStore.cs is a second admin-writable SQLite surface, added by WP-47 — after ADR-0004 (2026-07-20) — and its own doc-comment states the equivalence the ADR does not: "Runtime feature-flag state (WP-47). SQLite-backed like OrgTemplateStore, same single-gate idiom." It exposes Set(key, enabled) writing db.FeatureFlags, surfaced through the admin page /beheer/functies (apps/behandelportal/src/app/app.routes.ts:44-50, gated by capabilityGuard('flags:manage')).
  • Assessment — and why this is an ADR-fix, not a violation. The design is genuinely ADR-0004-shaped, not a breach of it: the catalog (which flags exist, their descriptions and defaults) is compiled-in code (Domain/Features/FeatureFlags.Catalog), only the boolean override persists, and IsEnabled fails closed for an unknown key (FeatureFlagStore.cs:45-47), so a bad DB row cannot invent a flag. That is the ADR's actual principle — schema and values gated at compile time — applied correctly. What is wrong is the ADR's text: it states the rule as a closed list of one exception, which means the next operational-config surface has no principle to test itself against and will either be waved through or blocked on a technicality.
  • Baseline citation: baseline §7 backend pattern inventory — the 7 static stores listed as "Not behind any port", which includes both OrgTemplateStore and FeatureFlagStore; baseline §3c (backend/Stamdata 96.8% line / 71.7% branch; backend/Data 99.0% / 75.5%).
  • Proposed resolution: amend ADR-0004. No code change; the code is right.
  • What the amended ADR should say: replace §"The deliberate exception: org-templates" with §"The deliberate exception: operational configuration", stating the test rather than a list — runtime-editable persistence is permitted only when (1) the catalog/schema of what may be set lives in code, (2) an unknown or unlisted key is rejected/fails closed, (3) the value is operational (per-organisation identity, an on/off rollout switch) and not a shared business rule whose wrong value breaks the register for everyone, and (4) writes are admin-capability-gated and audited. Then list the two surfaces that pass it today — OrgTemplateStore (WP-23/26) and FeatureFlagStore (WP-47) — and note that both are admin-gated. Add a matching sentence to CLAUDE.md §4, which currently repeats the singular framing ("Org-templates are the deliberate exception").
  • Blocked code tickets: any ticket proposing a third runtime-editable config surface should wait for this test to be written down, rather than arguing by analogy to org-templates.
  • Effort: S

ADR-0005 — OpenZaak (ZGW APIs) behind the BFF (Accepted)

Conformed, with no findings. This is the cleanest ADR in the set and I am recording that plainly rather than manufacturing a ticket.

Verified point by point:

  • The anti-corruption layer is in the .NET BFF and nowhere else. Every ZGW type lives under backend/src/BigRegister.Api/Zgw/ (8 files: OpenZaakZaakSource, OpenZaakDocumentSource, ZgwHttpClient, ZgwTokenProvider, ZgwZaakMapper, ZgwOptions, ZgwDiagnosticHandler, NotificatieDto). grep for OpenZaakZaakSource|OpenZaakDocumentSource outside Zgw/ returns only the two DI registrations in Program.cs:67,69 and eight explanatory comments — no ZGW shape reaches a consumer.
  • The config-switched port pair is exactly as decided: Program.cs:59-84 reads Zgw:Enabled and binds either OpenZaakZaakSource/OpenZaakDocumentSource (via AddHttpClient, with the WP-60 15s timeouts) or LocalZaakSource/LocalDocumentSource. Default is local, so the POC still runs fully offline as the ADR's second constraint requires.
  • The FE is untouched by the switch: consumers inject the interface only (Program.cs:206,281,340,425,434,446,473), and the same ApplicationSummaryDto is returned either way — zero DTO/api-client drift, matching the ADR's first +.
  • The ADR's stated minus is still accurate and honestly scoped: Program.cs:465 confirms "The local write runs against ApplicationStore directly (not the IZaakSource seam)", which is precisely the "only some endpoints have a source interface; each future slice introduces its own seam" consequence the ADR wrote down. An ADR that predicted its own remaining gap and the gap stayed where predicted is conformance, not drift.
  • backend/Zgw is the best-covered backend module at 98.1% line / 85.5% branch (baseline §3c) and the second-lowest duplication (1.8%, baseline §5) — the seam the ADR claimed would be "unit-testable without a live server" measurably is.

Note for agent 03: BL-006 (no NetArchTest/ArchUnitNET, one assembly) means nothing enforces the "ZGW shapes never leave Zgw/" property that this ADR depends on. It holds today by convention. That is agent 03's ticket to size, not mine — I record only that ADR-0005's conformance is currently review-maintained, not CI-maintained.


ADR-0006 — Test data through the production door (Accepted)

Core decision: partially conformed. Three of the five mechanisms are properly in place; the frontend replay idiom — the ADR's own flagship — is adopted in one spec out of nine.

Conformed:

  • §1 backend type-state builder — backend/tests/BigRegister.Tests/Builders/AanvraagBuilder.cs exists and is used in 5 test files (Acceptance/BesluitLifecycleTests.cs, Acceptance/IntakeSubmissionTests.cs, ApplicationTests.cs, Domain/BeoordelingRuleTests.cs, OpenZaakZaakSourceTests.cs) — the exact two files the ADR's Context named as the problem, plus three more.
  • §4 RemoteData named constructors — libs/shared/src/testing/remote-data.ts exists.
  • §5 e2e actors/seed-refs — e2e/support/actors.ts exists; no page-object layer was added.
  • No 'x' as BrandedType cast appears in any spec (grep for as Postcode|as Uren|as BigNummer|as Bsn in *.spec.ts: zero hits) — the §3 illegal route is genuinely closed.

ADR-C-010 — four machine specs hand-roll the exact state literal ADR-0006 §2 forbids

  • Type: code-violates-ADR
  • ADR cited: ADR-0006 §2 ("No object is built directly. A fixture is the result of running real Msgs through the real reduce") and the Decision table's "Pure reducer / state machine (frontend)" row: Do not"A literal returning { tag: 'Editing', ... } by hand"; plus §Consequences ("a hardcoded errors: {} fixture literal can't drift from what validation actually produces").
  • Evidence (verified) — each of these returns a state literal, and three of the four hardcode the errors: {} the ADR calls out by name:
    • apps/ssp/src/app/herregistratie/domain/intake.machine.spec.ts:19-25const answering = (...): IntakeState => ({ tag: 'Answering', answers, cursor, errors: {}, scholingThreshold }). This is the worst instance: apps/ssp/src/app/herregistratie/domain/intake.testing.ts exists in the same folder, exports givenIntake, and is the ADR's own quoted example (§2's code block is export const givenIntake = given(reduce, initial)) — yet givenIntake is imported only by intake.acceptance.spec.ts, never by the machine spec it was written for.
    • apps/ssp/src/app/registratie/domain/registratie-wizard.machine.spec.ts:25-31const invullen = (...): RegistratieState => ({ tag: 'Invullen', draft: {...}, cursor, errors: {}, upload: initialUpload }).
    • apps/behandelportal/src/app/behandeling/domain/besluit.machine.spec.ts:5-9const editingWith = (...): BesluitState => ({ tag: 'Editing', draft: {...}, errors: {} }).
    • apps/ssp/src/app/brief/domain/brief.machine.spec.ts:67-74const loaded = (...): BriefState => ({ tag: 'loaded', brief, availablePassages, decisions }).
    • For contrast, two specs do conform and should be the pattern to copy: libs/beheer/src/domain/stamdata-editor.machine.spec.ts:17-23 (seedLoaded() = reduce(initial, {tag:'Loaded', …})) and apps/ssp/src/app/brief/domain/org-template.machine.spec.ts:29-30 (reduce({tag:'loading'}, {tag:'DraftLoaded', …})). Only 1 of 9 machines has a *.testing.ts; change-request.machine.spec.ts:6 honours the idiom but declares given(reduce, initial) inline in the spec rather than in a *.testing.ts.
  • Baseline citation: baseline §7 FE pattern inventory (Elm-style machines: 9); baseline §3a — ssp/herregistratie 70.9% line / 67.8% branch and ssp/brief 75.3% / 68.8%, i.e. the two modules whose specs hand-roll states are also two of the three weakest-covered non-auth FE modules, which is consistent with fixtures asserting shapes the reducer may not actually produce.
  • Proposed resolution: add *.testing.ts next to each of the four machines (registratie-wizard, brief, besluit, plus adopt the existing intake.testing.ts), each a one-liner export const givenX = given(reduce, initial) per §2, and rewrite the four literal helpers as message replays. Where a state genuinely is not reachable by replay, that is a finding in its own right and should be recorded in the ticket rather than worked around — it means the reducer cannot produce a state the spec asserts.
  • Effort: M (four specs; mechanical but each needs the right message sequence worked out, and brief.machine.spec.ts is the largest)

ADR-C-011 — unwrapOk has zero adopters; its one call site reimplements it inline

  • Type: code-violates-ADR
  • ADR cited: ADR-0006 §3 ("Value objects → unwrapOk, never a cast") and the Decision table's "Value object / parser" row.
  • Evidence (verified): unwrapOk is defined in libs/shared/src/testing/value-object.ts and referenced nowhere else in apps/ or libs/ except libs/shared/docs/testing.mdx — zero spec consumers. The one place that needs it hand-rolls the same three lines: apps/ssp/src/app/registratie/application/submit-change-request.spec.ts:8-9 reads const telefoon = parseTelefoonnummer('0612345678'); if (!telefoon.ok) throw new Error('fixture phone should parse'); — semantically identical to unwrapOk(parseTelefoonnummer('0612345678')), which is exactly the duplication the ADR shipped the helper to remove.
  • Baseline citation: BL-004 (122 of 220 FE source files never loaded by any Vitest run — libs/shared/src/testing is one of the few modules at 100% reach, so a helper here that nothing imports is measurably dead weight, not merely unexercised); baseline §3a libs/shared/testing 3 files, 100% line.
  • Proposed resolution: one-line change in submit-change-request.spec.ts to call unwrapOk. Then judge honestly whether one call site justifies keeping the helper — if a future ticket finds it still has one consumer, deleting it and keeping the inline guard is the equally valid answer, and ADR-0006 §3's real requirement (never a cast) is satisfied either way. File it as "adopt or delete", not "adopt".
  • Effort: S

ADR-fix tickets (require architect approval)

Agent 08 must surface these five for human sign-off. None of them are code changes; all five change what the repo's architecture documents claim, and two of them gate code tickets.

ID ADR What the amendment does Gates Effort
ADR-C-001 0001 Rewrite the worked example against the real backend; drop the two discharged out-of-scope bullets nothing S
ADR-C-003 0001 State that the generated client is the wire contract post-codegen; scope contracts/ to codegen gaps; correct CLAUDE.md §4 any ticket deleting the 4 surviving contracts/*.dto.ts, or adding a hand-written DTO for a generated endpoint S
ADR-C-005 0002 ProposedAccepted; refresh §"Out of scope" (backoffice + status lifecycle + authz DTOs all shipped) ADR-C-004 — a Proposed ADR is weak grounds for a refactor ticket, so this must land first S
ADR-C-007 0003 Repoint five WP-67-stale paths; replace the false app-alert hand-rolled example; correct CLAUDE.md §2 nothing S
ADR-C-009 0004 Generalise "the org-templates exception" into a stated four-part test; list both passing surfaces; correct CLAUDE.md §4 any future third runtime-editable config surface S

Ordering dependency, called out explicitly as the brief requires: ADR-C-005 → ADR-C-004 → (re-measure BL-002) → possibly-nothing. ADR-C-006 is deliberately not in this chain: extracting the actor-agnostic route guards is compatible with ADR-0002 §3 as written and needs no approval. No ADR-fix is proposed against ADR-0002 §3's non-sharing rule. I considered it, as instructed, and rejected it: the rule's prediction has not been falsified, it has not been tested, because the change that would test it (ADR-C-004) was never made. Amending an ADR to match code that never executed its decision would ratify the omission rather than the evidence.


Summary

ADR Verdict Findings
0001 Decision conformed; prose stale ADR-C-001 (fix), 002 (code), 003 (fix)
0002 Decision conformed; §3 unexecuted; still Proposed ADR-C-004 (code), 005 (fix), 006 (code)
0003 Conformed; two documentation defects ADR-C-007 (fix), 008 (code/doc)
0004 Conformed; exception clause under-general ADR-C-009 (fix)
0005 Fully conformed — no findings
0006 Partially conformed; §2 adopted 1 of 9 ADR-C-010 (code), 011 (code)

Six code tickets (1×M, 1×M, 1×M, 3×S) and five ADR-fixes (all S). No finding proposes introducing a pattern that does not already exist in the repo, and every one cites a baseline observation or metric row.