## Scope: apps/ssp (auth, registratie, herregistratie, brief, showcase+shell+root), apps/behandelportal (auth, behandeling, shell+root), libs/shared (domain, application, infrastructure, ui, layout, kernel, upload, testing), libs/beheer, backend (Program.cs, Domain, Data, Zgw, Contracts, Stamdata, tests) ## Status: complete ## Last updated: 2026-08-26 ## Depends on: 00-baseline.md ## --- # 02 — Testability What blocks a **unit** test — static/singleton dependencies, hidden I/O, work in constructors/field initializers, pure logic entangled with impure. Every finding cites a `BL-###` or a metric row from `00-baseline.md`. Seams proposed are extractions, never rewrites. ## How this file reads the baseline Three filters were applied before anything was written down, and they killed more candidates than they kept: 1. **BL-004's carve-out is honoured.** No finding is filed against a `ui/` (or `layout/` component) file for lacking a Vitest spec. 66 Storybook stories + the a11y addon are the house strategy (CLAUDE.md §5), not a gap. 2. **"Untested" ≠ "untestable".** Several of the worst-covered non-`ui/` files are perfectly injectable and simply have no spec (`submit-besluit.ts`, `Contracts/Mappers.cs`, `breadcrumb-trail.ts`). Those are noted in their module section but not filed as testability findings — there is no seam to add. Whoever owns coverage should pick them up. 3. **Already-covered code needs a positive argument.** Backend line coverage is 97.6% (BL-005); a "this is untestable" claim there has to point at a branch the current test shape genuinely cannot reach. Two do (TE-007, TE-008); one points at the cost of how it is reached (TE-009). **Two baseline items are closed as false gaps** — see `libs/shared/domain` and `libs/beheer/contracts` below. BL-004 names both as "genuine gaps"; on inspection neither contains an executable statement. **Deliberate decisions engaged with, not overridden:** the 7 static backend stores (documented in `Data/Db.cs`) are left alone — TE-009 extracts rules _out_ of one of them without touching its shape. BL-002's auth duplication is respected — TE-004 lands the same seam twice rather than proposing a shared extraction. --- ## apps/ssp — auth **TE-001 — `SessionStore.restore()` reads `localStorage` inline, so its shape guard cannot be unit-tested** - Module / file:line — `apps/ssp/src/app/auth/application/session.store.ts:12-21` - **What blocks unit testing.** `restore()` is module-private and calls `localStorage.getItem(STORAGE_KEY)` itself, then does the parse + shape validation in the same function. It is invoked from a field initializer (`private _session = signal(restore())`, L37), so the storage read happens the instant the singleton is constructed. A spec cannot feed it a raw string; it must stub the `localStorage` global before the injector builds the store. The logic being guarded is not incidental — the comments mark it G1 (never persist the BSN) and G2 (validate the shape before trusting it), i.e. a trust boundary, and CLAUDE.md §5 mandates a spec for boundary `parse*` adapters. - **Baseline citation.** §3a: `ssp/auth` 42.9% line / 46.2% branch — **jointly the worst line coverage in the frontend table** (§8 ranking). Per-file lcov for this file: **LH 2 / LF 20 (10.0% line), BRH 3 / BRF 13 (23.1% branch)** — 4 of the module's 6 files are spec-reached (§3b, 67%), yet this one barely executes. - **Minimal seam.** Split the pure half out and move it next to the type it produces: `export function parseStoredSession(raw: string | null): Session | null` in `auth/domain/session.ts` — which **already has a spec file** (`auth/domain/session.spec.ts`) and is pure TS, so no new test scaffolding is needed. `restore()` collapses to `parseStoredSession(localStorage.getItem(STORAGE_KEY))`. Three test cases (absent, non-JSON, wrong shape) cover the guard. - **Effort S.** Independently shippable in one deploy — pure move, no call-site change outside the file. - **Note on BL-002.** `bhp/auth` carries the identical function; the seam lands **twice**, once per app. That is correct, not duplication to fix — ADR-0002 / CLAUDE.md §1 make `auth` deliberately unshared, and BL-002 flags any extract-to-shared here as contradicting an accepted ADR. Agent 06 owns whether that prediction still holds. ## apps/ssp — registratie **No findings.** The module's shape is the reason. Every `parse*` in its six adapters is exported and directly spec'd (`applications`, `big-register`, `brp`, `dashboard-view`, `duo` all have `.spec.ts` files); the machines are pure `domain/` units with specs; the five value objects each have one. `createDraftSync` deserves an explicit acquittal: at 143 lines it is the longest function in the repo (§4a, "Functions over 75 lines — the entire population") and it owns a `setTimeout` debounce, a `Router` navigation and an in-flight-create race guard. It is nevertheless **the best-seamed effectful unit in the frontend** — deps arrive through an explicit `DraftSyncDeps` object (`draft-sync.ts:24-33`), `Router`/`ActivatedRoute` are `inject(..., { optional: true })` so it is inert without them, and `enabled()` exists specifically so stories and tests can neutralize it (L31-32). It has a spec. Its length is agent 01's call, not a testability defect. `BigProfileStore` creates two `resource()`s in field initializers (constructor-time I/O), which is normally a blocker — but the store is pure glue over `parseDashboardView` (exported, spec'd) and `map`/`fromResource` (spec'd), so there is no untested decision hiding behind the construction. §3a: 80.0% line / 77.3% branch, §3b 51% reach with the 20 unreached files being 11 `ui/` components (BL-004) and 3 pure-type `contracts/` files. ## apps/ssp — herregistratie **No findings.** §3a 70.9% / 67.8%, §3b 56% reach. The four unreached files are the `ui/` pages and wizard organisms (BL-004) plus `intake-policy.store.ts`, a thin `resource()` wrapper over the exported-and-spec'd `parseIntakePolicy`. Both machines are pure, Angular-free and carry four spec files between them, including an acceptance spec. `intake.testing.ts` gives the wizard specs a fixture builder — the seam already exists. ## apps/ssp — brief **TE-002 — `RevealBigNummerAdapter` hides a trust boundary inside a global-`fetch` method** - Module / file:line — `apps/ssp/src/app/brief/infrastructure/reveal-bignummer.adapter.ts:32-41` - **What blocks unit testing.** The shape validation of the response body — the code's own comment calls it a "Trust boundary" — is written inline inside `async reveal()`, after an `await fetch(...)` on the **global** `fetch` (L24). There is no injected transport. To assert that a `{ bigNummer: 42 }` response is rejected, a spec must stub `globalThis.fetch`; the boundary itself is not callable. Every other `parse*` in the repo is exported (30 of them, §7) — this one is the outlier, and it guards a PII reveal (PRD-0002 §5c). The same shape recurs in the sibling hand-written-`fetch` adapters: `letter-preview.adapter.ts:56-62` (`errorMessage`) and `org-template.adapter.ts:82-89` (the proefbrief error mapping) — both un-exported, both unreachable without a `fetch` stub. - **Baseline citation.** §3b: `ssp/brief` **42% spec reach** (11 of 26 files) — the lowest of any non-zero context outside `bhp/behandeling`; §3a 75.3% line / **68.8% branch**. All three `fetch` adapters are among the 15 unreached files, and none of them is a `ui/` component, so BL-004's Storybook carve-out does not cover them. - **Minimal seam.** Export the pure half as a named boundary, matching the file's 30 siblings: `export function parseRevealed(body: unknown): Result` — five lines moved verbatim out of the method, which becomes `return res.ok ? parseRevealed(await res.json().catch(() => null)) : err(...)`. Same move for `errorMessage` in the other two adapters (already separate functions; they only need `export` + a spec). No transport abstraction, no `HttpClient` migration — the hand-written `fetch` stays, and the documented reasons for it (`.ExcludeFromDescription()`, per-request headers) are untouched. - **Effort S.** Independently shippable. ## apps/ssp — showcase, shell, root **No findings.** §3b lists `ssp/root` + `ssp/shell` (with the behandelportal equivalents) at **0% reach, 12 files**. That is the correct number for what these files are: `main.ts`, `app.config.ts`, `app.routes.ts`, `app.ts` and `shell/nav.config.ts` are composition roots and static data — a spec asserting a provider array restates it. `debug-state` is a dev-only devtool with a Storybook story. One honest note, no ticket: `shell/debug-state/mask.ts::redactProfile` is a pure, Angular-free PII-redaction function (it maps a `BigProfile` to a redacted shape) with no spec and **no blocker** — it is directly callable today. Its dependencies (`maskTail`, `REDACTED`) are in `libs/shared/kernel/pii.ts`, which is spec'd at 96.4%. Missing test, not blocked test. `showcase` is 100% line coverage on its one reached file (§3a); `snippets.generated.ts` is generated and `concepts.page.ts` is a teaching page. ## apps/behandelportal — auth **TE-001 applies here identically** — `apps/behandelportal/src/app/auth/application/session.store.ts:12-21`, same `restore()`, same `localStorage` read in a field initializer, same §3a row (`bhp/auth` 42.9% / 46.2%). Fix it in `apps/behandelportal/src/app/auth/domain/session.ts`, which also already has a spec. Counted once as TE-001; it is two commits, or one commit touching two apps. No additional findings. `medewerker.ts:17-22` reads `window.location.search` + `sessionStorage` directly, but it is a five-line dev-only role stand-in with the same shape as `libs/shared/infrastructure/role.ts` — whose twin `subject.ts` is spec'd, so the pattern is demonstrably testable as written. ## apps/behandelportal — behandeling **No findings.** §3b 31% reach (5 of 16) looks alarming and is not: the 11 unreached are 5 `ui/` files (BL-004), the three `resource()`-wrapper stores, and the two command factories. §3a records **91.6% line / 81.5% branch** on what is reached — the highest line coverage of any frontend module in the table. Explicitly not a testability finding: `application/submit-besluit.ts` is **structurally identical** to `apps/ssp/src/app/registratie/application/submit-change-request.ts`, which has a spec (`submit-change-request.spec.ts`). Same `inject()` + `runSubmit` factory, same signature shape. It is not blocked by anything; it is a missing spec whose template already exists in the repo. Both adapters' `parse*` functions are exported and spec'd (`beoordeling.adapter.spec.ts`, `werkvoorraad.adapter.spec.ts`). ## apps/behandelportal — shell, root **No findings.** Same composition-root reasoning as `ssp/shell + root` above. ## libs/shared — domain **No findings — BL-004's "genuine gap" is a false positive here, and can be closed.** §3b lists `libs/shared/domain` at **0% reached, 3 files**, and BL-004 names it first among "the genuine gaps are non-`ui/` files with no spec". Reading all three files (30 lines total): `capability.ts` is a 9-member string-literal union, `role.ts` is a 3-member union, `feature-flag.ts` is one `interface` plus one exported string constant. **There is no executable statement in the folder.** 0% is the correct and unimprovable number; the types are checked by `tsc` and their runtime counterparts are validated in `parseMe` (spec'd, 94.7% infrastructure coverage). No ticket should be written against this row. ## libs/shared — application **No findings.** §3a 80.3% / 70.0%, §3b 73% reach (8 of 11). The three unreached are `session.port.ts` (an `InjectionToken` + interface — a declaration, nothing to run), `feature-flags.store.ts` and `access.store.ts`. The seam kit itself (`remote-data`, `store`, `submit`, `history`, `pending-saves`, `machine-remote-data`, `debounced-save`) is fully spec'd — this is the folder that makes the rest of the frontend testable. Noted without a ticket: `AccessStore.can()` (`access.store.ts:34-37`) is a deny-by-default security gate whose decision reduces to `rd.tag === 'Success' && rd.value.includes(capability)` over a `resource()` created in a field initializer. The decision is two lines; the substance it guards (`parseMe`, where a real silent-deny bug shipped — see the WP-66 regression test in `me.adapter.spec.ts:26-31`) is already exported and thoroughly spec'd. Extracting a pure `canFrom(rd, cap)` would be honest but buys close to nothing. Filing it would be volume, not quality. ## libs/shared — infrastructure **No findings.** §3a 94.7% line / 81.0% branch, §3b 82% reach — the second-best module in the repo. BL-001 singles out `api-client.provider.ts:49 fetch` (CC 19) as one of only two CC>10 functions outside the mandated idioms, so it is worth stating why it is _not_ a testability finding: `httpClientFetch(http: HttpClient)` takes its dependency as an ordinary function parameter (L47) rather than injecting it, and it has a spec (`api-client.provider.spec.ts`). Its complexity is agent 01's call. The module-level mutable `pendingIdempotencyKey` (L21) is self-clearing in a `finally` (L25), so it does not leak between tests. ## libs/shared — ui **No findings — BL-004 governs.** 34 files, 13 reached; the unreached 21 are components covered by the Storybook + a11y strategy CLAUDE.md §5 mandates. The one non-component module in the folder, `rich-text-editor/rich-text-dom.ts` (home of `collect`, CC 11 — the other non-idiom CC>10 function per BL-001), **is** spec'd (`rich-text-dom.spec.ts`). The layer is doing what the house rules ask. ## libs/shared — layout **No findings.** §3b 18% reach (2 of 11) is the lowest non-zero row, but 8 of the 9 unreached are components (`shell`, `page-shell`, `site-header`, `site-footer`, `breadcrumb`, `language-switcher`, `wizard-shell`) — BL-004 applies to `layout/` exactly as to `ui/`, since CLAUDE.md §5 titles both under `Design System/`. Two non-component files, neither ticketed: - `breadcrumb/breadcrumb-trail.ts::trailFor` is a pure exported function with a subtle parent-walk and a `delete trail[last].link` mutation, and has no spec. **No blocker** — it is directly callable, and its sibling `language-switcher/locale-links.ts` is the spec'd proof. Missing test, not blocked test. - `route-focus.ts` is a 20-line `ENVIRONMENT_INITIALIZER` wrapping a `Router` subscription and `afterNextRender`. Genuinely awkward to unit-test, but it is a11y wiring with no branch worth asserting; a seam here would cost more than it returns. ## libs/shared — kernel **No findings.** §3a 96.4% line / 90.0% branch, §3b **100% reach** — the best module in the repo, and (§6) the most-depended-on at I = 5% with Ca 71. Pure functions, all spec'd. This is the reference standard the other findings point back at. ## libs/shared — upload Three findings. This module carries the frontend's weakest testability profile, and BL-010 already flags it as sitting outside the layer convention. **TE-003 — `UploadShellService` declares a port, then injects the concrete class instead** - Module / file:line — `libs/shared/src/upload/upload-shell.service.ts:12-24` and `:35` - **What blocks unit testing.** The file defines `export interface UploadTransport` and documents it as _the_ swap seam ("swapping it in touches only this interface", L10-11). It then binds it as `private transport: UploadTransport = inject(KeepaliveTransport)` (L35) — the **concrete class**, which is `@Injectable` but **not exported** (L18). A spec that wants a fake transport cannot reference the class to override its provider, and cannot provide against the interface (interfaces are not DI tokens). Result: every one of `upload()`, `delete()`, `cancel()` and `pollReturning()` — the code that translates transport outcomes into `UploadMsg`s — is reachable only through a real `XMLHttpRequest`. The port exists on paper and does nothing. - **Baseline citation.** §3a: `libs/shared/upload` 52.0% line / 50.0% branch — the worst line coverage of any module except the two `auth` rows. §3b: 50% reach, and per the lcov file list the **two unreached files are `upload-shell.service.ts` and `upload-controller.ts`** — neither is a `ui/` component, so this is exactly the non-`ui/` gap BL-004 says is genuine. - **Minimal seam.** Add the token the repo already uses elsewhere: `export const UPLOAD_TRANSPORT = new InjectionToken('UPLOAD_TRANSPORT', { providedIn: 'root', factory: () => inject(KeepaliveTransport) })`, then `inject(UPLOAD_TRANSPORT)` on L35. This **extends an existing pattern** — §7 records exactly one explicit port in the frontend, `SessionPort` + `SESSION_PORT` (`libs/shared/src/application/session.port.ts`), with the same interface-plus-token shape. Runtime behaviour is byte-identical; the default factory returns the same instance. - **Effort S.** Independently shippable in one deploy. **TE-004 — `createUploadController` performs injection, DOM subscription and an `effect()` at call time** - Module / file:line — `libs/shared/src/upload/upload-controller.ts:23-46`, policy at `:62-74` - **What blocks unit testing.** Calling the factory does four irreversible things before returning: three `inject()` calls (L24-25, L46), an `effect()` registration (L31), and `window.addEventListener('focus', onFocus)` (L45). It must therefore run inside a `TestBed` injection context with `UploadAdapter`, `UploadShellService` and `DestroyRef` all satisfied — and `UploadShellService` is itself un-fakeable per TE-003, so the mocking cost compounds. What is trapped behind that cost is real policy: `onFileSelected` (L62-74) decides per file whether to emit `FileRejected` with reason `'multiple'`, `FileRejected` with a `rejectReason` result, or to start an upload — a decision over `(categories, categoryId, files)` with no I/O in it. - **Baseline citation.** §3a `libs/shared/upload` 52.0% / 50.0%; §3b 50% reach with this file among the two unreached. §4a additionally records the module's `max CC 27` and the repo's only two >75-line functions include `reduceUpload` (109 lines) — the reducer this controller feeds. The reducer is spec'd (`upload.machine.spec.ts`); the code choosing _which_ messages reach it is not. - **Minimal seam.** Pure-function split into the file that is already the tested unit: add `export function planFileSelection(state: UploadState, categoryId: string, files: { name: string; type: string; size: number }[]): UploadMsg[]` to `upload.machine.ts`, moving L62-73 verbatim. `rejectReason` — the predicate it calls — is already exported from that file and already spec'd, so the move is downhill. The controller keeps the `crypto.randomUUID()` + `files.set()` + `shell.upload()` side effects and just executes the plan. No change to the controller's public surface or to the organism that calls it. - **Effort S.** Independently shippable. **TE-005 — `UploadAdapter.xhrUpload` buries response interpretation inside an `XMLHttpRequest` closure** - Module / file:line — `libs/shared/src/upload/upload.adapter.ts:113-157`, helpers at `:169-199` - **What blocks unit testing.** The method constructs `new XMLHttpRequest()` directly (L118) — no transport parameter, no injected factory — and attaches four listeners whose bodies contain the actual decisions: 2xx-vs-not (L131), `JSON.parse` of the body with a fallback (L132-136), ProblemDetails mapping via the un-exported `parseError` (L193-199), and abort-vs-error disambiguation (L142-144). None of it can be reached without stubbing the XHR global. Compounding it, the method also branches on `currentScenario()` at L115 and returns a `setTimeout`-driven dev simulator (`simulateUpload`, L169-192), so a dev-only fake and the production transport share one entry point. - **Baseline citation.** Per-file lcov: **LH 5 / LF 64 (7.8% line), BRH 3 / BRF 57 (5.3% branch)**. The file is counted as "reached" in §3b only because another spec imports it — essentially nothing in it executes. It is the single largest contributor to the module's 52.0% / 50.0% row in §3a. - **Minimal seam.** Extract the interpretation, not the transport: `export function uploadOutcome(status: number, responseText: string): Result` containing L131-139's logic plus `parseError`. The listener becomes a two-line dispatch into it. Optionally (same ticket, still small) move the `currentScenario()` branch from L115 up into `KeepaliveTransport.send()` — the seam TE-003 makes usable — so `xhrUpload` is transport only. Do **not** abstract `XMLHttpRequest`: the file documents why XHR is required (progress events + cancellation, which `fetch` cannot give) and that reason still holds. - **Effort S** for `uploadOutcome` alone, **M** if the scenario branch moves too. Independently shippable; sequence it after TE-003 if both are taken. ## libs/shared — testing **No findings.** §3a 100% line coverage. `given()` (`machine.ts`) and the `RemoteData` constructors (`remote-data.ts`) are the DSL the domain specs are built on, and the `no-testing-in-production` dependency-cruiser rule (§6) keeps them out of shipped code. The gap this folder does _not_ yet cover is a `resource()`-shaped fake — which is why `AccessStore`/`BigProfileStore` stay unreached — but adding one is a test-infrastructure task, not a source-code seam, and no metric row demands it. ## libs/beheer **TE-006 — blob-to-browser handoff is inlined in three application-layer commands** - Module / file:line — `libs/beheer/src/application/stamdata.store.ts:137-147`; also `apps/ssp/src/app/brief/application/brief.store.ts:230` and `apps/ssp/src/app/brief/application/org-template.store.ts:217` - **What blocks unit testing.** Each of the three commands ends in raw DOM/browser API calls that jsdom cannot meaningfully execute: `StamdataStore.download()` does `URL.createObjectURL` → `document.createElement('a')` → `a.click()` → `URL.revokeObjectURL`; `BriefStore.previewLetter()` and `OrgTemplateStore.proefbrief()` both do `window.open(URL.createObjectURL(blob), '_blank')`. Because the call is the **last statement**, the entire success path of each command is unassertable — a spec can only exercise the early-return/failure branches. `brief.store.spec.ts` demonstrates this exactly: it tests `previewLetter`'s failure case (which returns at the `!r.ok` guard) and cannot test the success case. In `download()` the untestable tail sits directly behind a two-clause guard (`if (!s || !this.canDownload()) return;`, L139), so the guard's true-branch is permanently dark. - **Baseline citation.** §3a: `libs/beheer/application` **40.5% branch — the worst branch coverage of any frontend module in the table**, and its 65.7% line figure is third-worst. Per-file lcov confirms `stamdata.store.ts` _is_ that row: LH 46 / LF 70, **BRH 15 / BRF 37**. On the brief side, §3a `ssp/brief` is 68.8% branch and `brief.store.ts` measures **BRH 32 / BRF 64 — exactly 50%**. - **Minimal seam.** One small injectable in `libs/shared/src/application`, mirroring the `SESSION_PORT` token shape already in that folder: `export const BLOB_PRESENTER = new InjectionToken<{ open(b: Blob): void; download(b: Blob, filename: string): void }>('BLOB_PRESENTER', { providedIn: 'root', factory: () => realBlobPresenter })`. The three commands each lose 1-4 lines of DOM code and gain one method call; specs provide a recording fake and finally assert the success paths (including `toJson(...)`'s output actually reaching the file, which today is only tested one level down in `beheer/domain`). The content-producing logic stays exactly where it is. - **Effort S** (one token + three one-line edits) — **M** including the specs the seam unlocks. Independently shippable; the three call sites can also land separately. **Other beheer layers — no findings.** - `libs/beheer/contracts` — §3b lists it at **0% reached, 1 file**, and BL-004 names it as a genuine gap. It is not: `stamdata.dto.ts` is 30 lines of `interface` and `type` declarations with **zero executable statements** and, by design, zero imports (it is the wire seam). Like `libs/shared/domain`, this row should be closed rather than ticketed. - `libs/beheer/ui` — 0% reach, 4 files, all components → BL-004 / Storybook. - `libs/beheer/domain` — 98.1% line, spec'd machine and rules. Nothing blocked. - `libs/beheer/infrastructure` — `parseStamdataTable`/`parseColumn`/`parseRows` all exported and spec'd; the 60.5% branch figure is unexercised defensive arms in an otherwise open unit. --- ## backend/Program.cs **No findings.** §3c: 97.4% line / 84.8% branch, the second-best branch figure on the backend. Ten endpoint bodies read the wall clock inline (`DateTimeOffset.UtcNow` at L282, L286, L301, L390, L394, L426, L435, L449, L482, L931; `DateOnly.FromDateTime(DateTime.Today)` at L138), which would normally be a finding — but **every rule and mapper they hand it to already takes the instant as a parameter**: `HerregistratieRule.Evaluate(reg, today)`, `ToDetailDto(a, now)`, `ToStatusDto(a, now)`, `ListCases(now)`, `ApplicationStore.RecordBesluit(..., now)`. The clock-dependent _decisions_ are all testable at any date; only the endpoint wiring is pinned to now, and that wiring is what `EndpointTests`/`AdminCasesTests` legitimately cover through the host. Injecting `TimeProvider` into 48 minimal-API lambdas would be a rewrite, not a seam, and no metric row asks for it. BL-003 (940 lines, file CC 78, read/write split by comment banner) is a **structure** finding, explicitly, and belongs to agents 03/04. ## backend/Domain **TE-007 — `LetterHtml.ResolveAuto` reads the wall clock although `Render` is already given the instant** - Module / file:line — `backend/src/BigRegister.Api/Domain/Letters/LetterHtml.cs:138` (resolver) vs. `:27` (signature) and `:49` (the correct usage) - **What blocks unit testing.** `Render(BriefEntity brief, OrgTemplateDto template, string at, bool watermark)` already accepts the letter's instant, and uses it properly for the letterhead: `sb.Append(Enc(FormatDatumNl(at)))` at L49. But the body's `datum` placeholder resolves through `ResolveAuto`, which ignores `at` and calls `FormatDatumNl(DateTimeOffset.UtcNow.ToString("o"))` (L138). `ResolveAuto` is `private static`, reached only via `RenderNode` ← `RenderParagraphs` ← `Render`, so a test has no way to pin the value: it can only assert "whatever today is". The gap is visible in the existing test file — `LetterHtmlTests.cs:26` declares a `new PlaceholderDefDto("datum", "Datum", true)` in the fixture and **no assertion anywhere in the file checks what it renders to**. This is a pure `Domain/` rule class reaching for ambient state, which is precisely the purity §7 credits the folder with. - **Baseline citation.** §3c: `backend/Domain` **82.0% branch** — the third-weakest branch axis (BL-005). §4b: `Domain/Letters/LetterHtml.cs` file **CC 21**, the third-highest-CC file in the backend after `Program.cs` (78) and `Data/ApplicationStore.cs` (27). - **Minimal seam.** Thread the parameter that already exists: `ResolveAuto(string key, string label, string at)` → `"datum" => FormatDatumNl(at)`, passing `at` down through `RenderParagraphs`/`RenderNode` (both private, both already in `Render`'s call chain with `at` in scope). Two signature changes, one expression change, zero public API change, zero call-site change. Then assert the rendered `datum` against a fixed expected string in `LetterHtmlTests`. - **Secondary benefit, stated conservatively.** This is not a shipped bug today — every caller (`Program.cs:697`, `:708`, `BriefStore.cs:120`) passes `Now()` at render time, so the two dates coincide. It becomes one the moment `Render` is called with a historical `at` (re-rendering an archive, back-dating a letter), at which point the letterhead and the body would disagree within a single document. - **Effort S.** Independently shippable. No other Domain findings. §7's claim holds under inspection: `SubmissionRules`, `DocumentRules`, `IntakePolicy`, `BeoordelingRules`, `DiplomaRules`, `HerregistratieRule`, `OrgTemplateRules`, `Authz` and `FeatureFlags` are static classes of pure functions with a matching file in `tests/Domain/`, and the clock-dependent ones take their instant as an argument. That is the correct shape. ## backend/Data **TE-008 — brief state-transition and authorization rules live inside DB-opening, lock-held store methods** - Module / file:line — `backend/src/BigRegister.Api/Data/BriefStore.cs`, five guard clusters: `:72-76` (Save), `:88-90` (Submit), `:111-112` (Send), `:162-164` (Approve/Reject shared path), plus the `RequiredFilled(e)` predicate - **What blocks unit testing.** Each guard is a pure decision over `(status tag, actor role, entity completeness)` — e.g. `Save` returns `Forbidden` if `!isDrafter`, `Conflict` unless the status is `draft` or `rejected`, and reopens a `rejected` letter to `draft`; `Submit` additionally requires `RequiredFilled`. But each sits **inside** a method that has already done `lock (_gate) { using var db = Db.Create(); ... }`, so exercising any of them requires a booted host and a real SQLite file. There is no `BriefRules` class: `Domain/Letters/` contains only `LetterHtml.cs` and `OrgTemplateRules.cs`. The pattern is visibly **half-applied** — `Authz.CanActOn` at L163 _is_ a pure `Domain/` call, sitting one line away from three guards that are not. The `Save` guard's own comment says it "mirrors the FE reducer", i.e. it is business logic with a known pure counterpart on the other side of the wire. - **Baseline citation.** §3c: `backend/Data` **75.5% branch** — named in BL-005 as one of the three weak branch axes, against 99.0% line coverage (the exact signature of "every unit is entered, edge branches are not"). §4b: `Data/BriefStore.cs` file **CC 17**, and its `ToDto` at **CC 16** is the highest-CC non-`Program.cs` method in the backend. §5: `backend/Data` 7.7% duplication. - **Minimal seam.** Add `Domain/Letters/BriefRules.cs` with pure statics — `CanSave(BriefStatusDto status, bool isDrafter) → Outcome`, `StatusAfterSave(BriefStatusDto) → BriefStatusDto`, `CanSubmit(status, isDrafter, bool requiredFilled) → Outcome`, `CanSend(status)`, `CanDecide(status, Principal, drafterId)` — and have each store method call one. The store keeps its lock, its `Db.Create()`, its static shape and its signature; only the `if` cascade moves. This **extends the pattern §7 already records** for `SubmissionRules` / `BeoordelingRules` / `OrgTemplateRules` / `DocumentRules`, and adds a `tests/Domain/BriefRuleTests.cs` alongside the seven that exist. - **Explicitly NOT proposed: changing the static-store shape.** `Data/Db.cs:6-12` documents the static, non-DI store decision, and `tests/TestWebApplicationFactory.cs:1-12` states the position outright — "Serializing test classes is the fix, not a redesign of the stores for a test-only concern." TE-008 respects that completely: it is orthogonal, and works _because_ the rules never needed the DbContext in the first place. - **The cost this seam actually pays down.** Because `Db.ConnectionString` is one static field, that same file carries `[assembly: CollectionBehavior(DisableTestParallelization = true)]` — **all 241 backend tests run serially, process-wide**, and every brief-rule assertion currently pays a host boot + SQLite file for a decision that is a pure function of two enums. Each rule moved out of `BriefStore` moves a test out of the serialized integration lane into the free-running unit lane. That is the argument for the seam; it is not an argument for touching the stores. - **Effort M** (five extractions + one new test file). Independently shippable, and splittable one method at a time if preferred. Two smaller Data notes, neither ticketed: `IdempotencyStore` is the only store that is purely in-memory with no `Reset()` and no TTL, so its dictionary survives `TestWebApplicationFactory` disposal and is shared by every test class in the process — harmless today only because `IdempotencyTests.cs:24` keys on `Guid.NewGuid()`. And `DocumentStore.cs:54` / `AuthzAuditStore.cs:35` stamp `DateTimeOffset.UtcNow` inline while `ApplicationStore.RecordBesluit` correctly takes `now` — an inconsistency, but neither audit timestamp is asserted on, so no metric supports a ticket. ## backend/Zgw **No findings.** §3c 98.1% line / **85.5% branch — the strongest branch figure on the backend**, and §5 records 1.8% duplication. This is the module that was built as ports-and-adapters from the start (ADR-0005): `IZaakSource`/`IDocumentSource` each have two implementations (§7), `ZgwHttpClient` takes an injected `HttpClient`, and `tests/ZgwStubHandler.cs` provides the transport fake — five test files ride on it. It is the backend's worked example of the seam TE-003 asks the upload module for. ## backend/Contracts **No testability findings — but state the gap accurately.** §3c records `backend/Contracts` at **65.0% branch, the worst branch figure in the repo** (BL-005 names it first). It is nonetheless not a testability finding: `Mappers.cs` is 79 lines of pure `static` extension methods over records, with the clock already injected where it matters (`ToStatusDto(this Aanvraag a, DateTimeOffset now)` at `:52`, `ToSummaryDto(..., now)` at `:68`, `ToDetailDto(..., now)` at `:76`), and `Dtos.cs` is 250 lines of `record` declarations. §4b confirms the shape: file CC 4, max method CC 3, the lowest complexity of any backend folder. **Nothing blocks a unit test here.** What is missing is a test _file_: `backend/tests/` has `Domain/`, `Acceptance/` and `Builders/` folders but no `Contracts/`, so all 65% is incidental coverage picked up through endpoint tests. That is a coverage ticket for whoever owns coverage, requiring zero source change — and per BL-009 there is no ratchet, so it would have to be verified against §3c's numbers by hand. ## backend/Stamdata **TE-009 — `Professions.ByProgram` freezes its valid-time filter at type-load from `DateTime.Today`** - Module / file:line — `backend/src/BigRegister.Api/Stamdata/Professions.cs:25-27` - **What blocks unit testing.** `ByProgram` is a `static readonly IReadOnlyDictionary` whose initializer runs `Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, DateOnly.FromDateTime(DateTime.Today)))`. Two compounding problems: the peildatum is the ambient wall clock, **and** the result is computed once per process at type-load and then immutable. A test cannot ask "which mappings are active on 2030-01-01" — not by arranging state, not by ordering, not at all. The temporal behaviour of the one business-tunable table that has a validity window is therefore unreachable. The file's own comment concedes the consequence: it "preserves the pre-valid-time behaviour exactly **while the file's rows are all current**" — i.e. the `ActiveOn` call is presently a constant-true filter, so both of its interesting branches (not-yet-valid, expired) are dead in every run. - **Baseline citation.** §3c: `backend/Stamdata` 96.8% line but **71.7% branch** — named in BL-005 as the second-weakest branch axis, an exact 25-point line/branch split. §4b: `Stamdata/StamdataTable.cs` file CC 21, joint-third-highest in the backend. This is the rare backend case where "untestable" is defensible against 97.6% line coverage: the lines run, the branches provably cannot. - **Minimal seam.** Add the parameterized overload and define the existing field in terms of it: `public static IReadOnlyDictionary ByProgramOn(DateOnly on) => Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on)).ToDictionary(...);` then `public static readonly IReadOnlyDictionary ByProgram = ByProgramOn(DateOnly.FromDateTime(DateTime.Today));`. **Zero call-site changes** — `DiplomaRules.ProfessionFor` and `All()` keep using `ByProgram`. `StamdataValidationTests` gains the ability to assert both validity-window branches against authored future/expired rows. - **This extends an existing pattern in the same folder.** `StamdataTable.cs:63` already does exactly this — `Temporal ? Rows().Where(r => ActiveOn(r, on)).ToArray() : Rows()`, with `on` as a parameter — and `StamdataFile.ActiveOn(van, tot, on)` (`:36`) is already clock-free. `Professions.cs` is the one caller that swallows the parameter. - **Effort S.** Independently shippable; additive only. ## backend/tests **No findings.** 39 files, 4 253 lines, 241 green tests, every backend source file reached (§3c). The suite already carries the fixtures a unit lane needs (`Builders/AanvraagBuilder.cs`, `ZgwStubHandler.cs`, `TestWebApplicationFactory` with per-class throwaway SQLite files). The one structural observation is not a defect to fix here: `[assembly: CollectionBehavior(DisableTestParallelization = true)]` (`TestWebApplicationFactory.cs:12`) serializes the entire suite because `Db.ConnectionString` is a process-global. The repo reached that decision deliberately and documented the race it prevents. Rather than reopen it, TE-008 and TE-009 reduce how much _needs_ to run in that serialized lane. Note also §4b's outlier in this folder: a **293-line test method at CC 20** (`CreateZaak_posts_zaak_status_and_rol_and_maps_the_result_back`, `OpenZaakZaakSourceTests.cs`) — a test-readability item for agent 01, not a testability seam. --- ## Summary | ID | Title | Module | Blocker | Baseline | Effort | 1 deploy | | ------ | -------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------ | ------ | -------- | | TE-001 | `SessionStore.restore()` reads `localStorage` inline | ssp/auth + bhp/auth | hidden I/O in a field initializer; guard is module-private | §3a 42.9%/46.2% (worst line); file LH 2/20, BRH 3/13 | S ×2 | yes | | TE-002 | Trust boundary hidden inside a global-`fetch` method | ssp/brief | un-exported shape validation behind `await fetch` | §3b 42% reach; §3a 68.8% branch | S | yes | | TE-003 | `UploadTransport` port declared, concrete class injected | libs/shared/upload | `inject(KeepaliveTransport)`; class not exported | §3a 52.0%/50.0%; file unreached (§3b, non-`ui/`) | S | yes | | TE-004 | `createUploadController` injects + binds `window` at call time | libs/shared/upload | 3× `inject()`, `effect()`, `addEventListener` before returning | §3a 52.0%/50.0%; file unreached (§3b, non-`ui/`) | S | yes | | TE-005 | `xhrUpload` interprets responses inside an XHR closure | libs/shared/upload | `new XMLHttpRequest()` hard-coded; dev simulator shares the method | file LH 5/64 (7.8%), BRH 3/57 (5.3%) | S–M | yes | | TE-006 | Blob-to-browser handoff inlined in 3 commands | libs/beheer + ssp/brief | `window.open` / `a.click()` as the last statement of each command | §3a beheer/application 40.5% branch (worst); brief.store 50% | S–M | yes | | TE-007 | `LetterHtml` resolves `datum` from `UtcNow`, not from `at` | backend/Domain | ambient clock in a private resolver inside a pure rule class | §3c Domain 82.0% branch; §4b file CC 21 | S | yes | | TE-008 | Brief transition rules live inside DB-opening store methods | backend/Data | 5 pure guards behind `lock` + `Db.Create()` | §3c Data 75.5% branch (BL-005); §4b CC 17, `ToDto` CC 16 | M | yes | | TE-009 | `Professions.ByProgram` freezes valid-time at type-load | backend/Stamdata | `static readonly` + `DateTime.Today`; both branches unreachable | §3c Stamdata 71.7% branch (BL-005) | S | yes | **Modules with no findings:** ssp/registratie · ssp/herregistratie · ssp/showcase+shell+root · bhp/behandeling · bhp/shell+root · libs/shared/{domain, application, infrastructure, ui, layout, kernel, testing} · libs/beheer/{domain, infrastructure, ui, contracts} · backend/Program.cs · backend/Zgw · backend/Contracts · backend/tests. **Baseline rows recommended for closure as false gaps:** `libs/shared/domain` (0% reach, 3 files) and `libs/beheer/contracts` (0% reach, 1 file) — both named in BL-004 as genuine gaps; both contain only type declarations and no executable statement. **Cross-references, not owned here:** BL-001 complexity (agent 01) · BL-002 auth duplication (agent 06) · BL-003 `Program.cs` structure (agents 03/04) · BL-006 backend architecture enforcement (agent 03) · BL-007 write-side placement (agent 04) · BL-008 `coverageExclude` · BL-009 no coverage ratchet — which means none of the findings above can be verified as "improved" by CI alone; verify against `00-baseline.md`'s numbers · BL-010 `libs/shared/upload` layer placement (TE-003/004/005 all land inside that carve-out and do not resolve it) · BL-011 suite flakiness under parallel load.