docs: archive the finished backlogs (RD-30)

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-09-08 23:00:38 +02:00
co-authored by Claude Opus 5
parent 097e8468e0
commit 12f17d9d73
161 changed files with 154 additions and 24 deletions
@@ -0,0 +1,641 @@
## 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%) | SM | 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% | SM | 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.