## 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 value_ — `apps/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` + ``, 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.ts` → `libs/shared/src/infrastructure/`, `upload.machine.ts` (+ its spec) → `libs/shared/src/domain/`, and `upload-controller.ts`/`upload-shell.service.ts` → `libs/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..js`'s `-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` (or have the guard derive it from `session() !== null`, which both `SessionStore`s 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/auth` ↔ `bhp/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.scss` → `libs/shared/styles.scss`; `src/index.html` → `apps/ssp/src/index.html` **and** `apps/behandelportal/src/index.html` (two now, not one); `.storybook/` → `.storybook-ssp/` and `.storybook-behandelportal/`; `src/docs/cibg-gaps.mdx` → `libs/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 `Msg`s 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-25` — `const 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-31` — `const invullen = (...): RegistratieState => ({ tag: 'Invullen', draft: {...}, cursor, errors: {}, upload: initialUpload })`. - `apps/behandelportal/src/app/behandeling/domain/besluit.machine.spec.ts:5-9` — `const editingWith = (...): BesluitState => ({ tag: 'Editing', draft: {...}, errors: {} })`. - `apps/ssp/src/app/brief/domain/brief.machine.spec.ts:67-74` — `const 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 | `Proposed` → `Accepted`; 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.