Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/02-testability.md
T
ehoandClaude Opus 5 664a43bf2d docs: refactoring-backlog workspace — baseline + 3 Phase 1 agents
Runs the multi-agent refactoring-backlog pipeline in docs/project/
refactor-backlog-setup/ up to and including three of the seven Phase 1
agents.

00-baseline.md establishes the metrics every later agent must cite, using
only tooling already in the repo (vitest lcov, coverlet cobertura, ESLint's
core `complexity` rule at threshold 0 for a full distribution, depcruise
--metrics). Duplication and C# complexity had no tooling, so
tools/baseline-scan.mjs adds a deterministic ~200-line text scan rather
than a new dependency; the approximations are labelled as such.

Headline: FE 75.1% line coverage but only over the 98 of 220 source files a
spec loads; BE 97.6% line / 79.6% branch; 0 layering violations; 7.1%
duplication; 25 of 2085 TS functions over CC 10.

Then 02-testability, 04-cqrs-light and 06-adr-conformance (27 findings).
01/03/05 were skipped deliberately — the baseline shows little for them to
find; 07 (BIO2) and 08 (consolidation) are still open.

Each agent corrected a baseline observation of mine, and in every case the
error was in something derived rather than measured:

- BL-007 counted ~13 adapter "mutations" from the `runSubmit` helper name;
  5 of those call sites are reads. It also missed 3 real mutations that
  reach the raw ApiClient and never return a Result.
- BL-002 diagnosed the 100%-duplicated auth folders as ADR-0002's
  divergence prediction failing. It never had a chance to fail: §3's
  `Principal` union was never built.
- BL-004 named libs/shared/domain and libs/beheer/contracts as coverage
  gaps; both are pure type declarations where 0% is unimprovable.

All three corrections are recorded inline in 00-baseline.md §10, so agent
08 does not inherit the bad numbers.

.prettierignore excludes the agent prompt directories — reflowing their
markdown would edit the prompt text itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:44:32 +02:00

642 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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<Session | null>(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<string, string>` —
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<UploadTransport>('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<string,
{ documentId: string }>` 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<string,string> ByProgramOn(DateOnly on) => Mappings.Where(m => StamdataFile.ActiveOn(m.GeldigVan, m.GeldigTot, on)).ToDictionary(...);`
then `public static readonly IReadOnlyDictionary<string,string> 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.