TE-006: StamdataStore.download(), BriefStore.previewLetter() and OrgTemplateStore.proefbrief() each ended in raw DOM blob calls jsdom cannot meaningfully execute, so their success paths were unassertable and download()'s two-clause guard true-branch was permanently dark. BLOB_PRESENTER mirrors the SESSION_PORT shape; all three commands go through it. download()'s branch coverage goes from 40.5% to 67.6%, and org-template.store.ts gets its first spec at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> # Conflicts: # docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md # libs/shared/docs/behaviour-spec.mdx
46 KiB
Scope: all findings from 00-baseline, 02-testability, 04-cqrs-light, 06-adr-conformance, 07-bio2-compliance — deduplicated, scored, CD-sequenced
Status: complete
Last updated: 2026-08-27
Depends on: 00-baseline.md, 02-testability.md, 04-cqrs-light.md, 06-adr-conformance.md, 07-bio2-compliance.md
---
99 — Consolidated refactoring backlog
47 findings in, 33 open tickets + 5 ADR-fixes + 1 shipped set out. Everything below
traces to at least one TE-/CQ-/ADR-C-/BIO- finding and cites a baseline metric.
HALT lifted 2026-08-27 — the operator approved the backlog and Phase 3 started.
CD batch 1 (RB-01..RB-06) is implemented, one commit per ticket on branch
refactor/adr-c-006-shared-route-guards, each with a note in implementation/rb-0N.md.
Batches 2–6 are untouched. The Status column below is the source of truth.
Two batch-1 findings had knock-on effects a later ticket must not re-derive:
- RB-01's residual is RB-09's problem. Both callers of the document-content endpoint
reach it as a plain browser navigation (
<a href>/previewUrl), carrying no identity header and passing through no interceptor, soStubIdentityProvideranswers with the seeded citizen. The links keep working only because one citizen owns every document in the POC. That is BIO-002; RB-09 needs this endpoint to receive a real credential. - RB-06 also deleted
SubmissionRules.RejectRegistratie, which the row did not ask for. It was reachable only from the deleted endpoint and contradicted by the live submit path. Recorded as the ticket's one judgement call inimplementation/rb-06.md.
Pii.MaskTail now lives in Domain/People/Pii.cs (moved out of Program.cs by RB-03) —
RB-11 and any later redaction work should use it rather than hand-rolling a second copy.
Coverage of this backlog — read this before treating it as complete
Three of the seven Phase 1 agents were deliberately skipped by the operator
(reasons recorded in _status.md). This backlog therefore contains no findings of the
following kinds, and their absence is not evidence that none exist:
| Agent not run | Category of finding that is absent |
|---|---|
| 01 — readability | Function/file length, naming, nesting depth, comment quality, dead code, test readability. No ticket below is a "this is too long/unclear" ticket. |
| 03 — DDD/hexagonal | Backend layering, vertical-slice structure, port extraction, module boundaries. The backend's structure is untouched except where CQRS-light reached it. |
| 05 — BDD | Nothing material — the agent self-reduced to a structural note; gen:behaviour-spec already covers the intent. |
Concrete consequences, so nobody assumes these were considered and dismissed:
createDraftSync(143 lines, the longest function in the repo, §4a) is only partly addressed. RB-21 splits its read half out on CQRS grounds. Whether the remainder is still too long was never assessed.- The other named length/complexity candidates have no owner:
api-client.provider.ts:49 fetch(CC 19) andrich-text-dom.ts:130 collect(CC 11) — the only two CC>10 functions outside the mandated idioms per BL-001; the 293-line CC-20 test method inOpenZaakZaakSourceTests.cs; and the six files over 400 lines (§9). RB-19 reordersProgram.csbut does not shorten it. - Backend structure was assessed only through the CQRS-light lens. BL-003's
invitation (940 lines →
Features/) is filed as out-of-mandate OOM-A, not a ticket. BL-010 (libs/shared/upload/outside the layer convention) is resolved only incidentally, by RB-24, which came from the ADR agent rather than the structure agent. - Two baseline observations remain unowned by any agent: BL-005 (backend branch
coverage 18 points behind line coverage;
Contracts65.0%,Stamdata71.7%,Data75.5% —backend/tests/has noContracts/folder at all) and BL-009 (no coverage ratchet anywhere). Neither is a testability blocker, so agent 02 correctly declined both; they are coverage work with no seam to add, and no ticket below covers them.
Already done — implemented and committed, do not re-file
Branch refactor/adr-c-006-shared-route-guards, five commits.
| Finding | Commit subject | Status | Residual |
|---|---|---|---|
| ADR-C-005 | docs(adr-0002): accept, and record the unbuilt Principal union as debt |
implemented | ADR-0002 is now Accepted, so RB-13 (ADR-C-004) now stands on a correct ADR — that was the whole point of the gate. |
| ADR-C-006 | refactor(auth): share the actor-agnostic route guards (ADR-C-006) |
implemented | Auth duplication 211 → 151 lines. §5's ssp/auth 100% / bhp/auth 86.8% rows and the auth.guard* clone pairs in the baseline are now stale — re-measure before citing them. Standing compliance criterion from agent 07: any future change to authGuard/capabilityGuard is an access-control change and must re-run the guard spec for both apps. |
| CQ-004 | fix(flags): surface a failed admin toggle instead of swallowing it |
implemented | Half of its compliance criterion is unmet. Agent 07 required "fix the FE error and the BE audit row together". The FE error shipped; PUT /admin/flags/{key} still writes no audit row. That half is carried by RB-07, and it is why ADR-C-009 must not be signed off before RB-07 lands. |
| TE-009 | fix(stamdata): evaluate the profession validity window per call, not at type-load |
implemented | Also closed the latent dead-ActiveOn-branch bug. Not compliance-flagged. |
| BL-008 | build: make coverageExclude actually exclude the generated API client |
implemented | The reported libs/shared/infrastructure figure should now read ≈94.7%, not 6.9%. §3a is stale on that row. |
Correction to the hand-off. The brief listed "CQ-002/004 (FeatureFlagStore.set)" as
fixed. Only CQ-004 was — FeatureFlagStore.set is the CQ-004 subject. CQ-002
(ApplicationsStore.cancel, AdminCasesStore.delete) is verified still open: both
still do try { await this.adapter.x(id) } catch { this.state.set(before) } with no
runSubmit, no Result, and no error channel. It is filed below as RB-20.
The backlog
How to read the CD batch column. A batch is a suggested ordering wave, not a release
train. Every ticket in the table ships alone, on its own merge, without any other
ticket in its batch. Where a ticket genuinely cannot ship alone it was split into a chain
(RB-22/RB-23) — see "Tickets that were rejected and split". Depends on means must be
deployed first, not must ship together.
Compliance column. SIGN-OFF = requires compliance sign-off before merge, per rule 4.
Every ticket tracing to a BIO- finding, plus every row on agent 07's authoritative
16-row "Compliance review required" list, carries it — regardless of priority.
| ID | Module | Category | Description | Baseline metric improved | Effort | Risk | Priority | CD batch # | Depends on | Compliance | Status |
|---|---|---|---|---|---|---|---|---|---|---|---|
| RB-01 | backend/Program.cs + Data | security | Add an owner/capability check to GET /uploads/{id}/content and /uploads/status; 404 not 403 |
§3c Data 75.5% branch vs 99.0% line (BL-005) | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-02 | backend/Program.cs + Data | privacy | Stop concatenating the BSN into AuthzAudit.Resource; assert on values in the test |
§3c Data 75.5% branch (BL-005) | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-03 | backend/Contracts | privacy | MaskTail(a.Owner, 3) in ToAdminSummaryDto — both cross-owner lists inherit it |
§3a bhp/behandeling 91.6%/81.5%; §7 Mapping row | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-04 | backend/Data | privacy | Mask the BSN used as AuditEntry.Actor on document audit rows (ownership column untouched) |
§3c Data 99.0% line / 75.5% branch | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-05 | backend/Zgw | privacy | Drop the BSN-bearing query + body snippet from the ZgwHttpClient exception message |
§3c Zgw 98.1%/85.5% (best backend branch) — a design gap, not a test gap | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-06 | backend/Program.cs | security | Delete the dead POST /registrations (no FE caller) — or add the ForeignIds guard |
BL-003 (48 mappings in 940 lines, file CC 78) | S | Low | P1 | 1 | — | SIGN-OFF | done |
| RB-07 | backend/Program.cs | audit | Audit the allow path in all five authz gates + the 3 brief transitions and the besluit | §3c Program.cs 84.8% branch; BL-003 | S–M | Med | P1 | 2 | — | SIGN-OFF | done |
| RB-08 | backend/Program.cs | security | Route DELETE /admin/uploads/{id} through CasesAdmin; delete the orphaned IsAdmin gate |
BL-003; §7 CQRS-light wrappers row | S | Low | P1 | 2 | RB-07 | SIGN-OFF | done |
| RB-09 | backend/Domain + Program.cs | security | IIdentityProvider can express "no identity"; stub Development-only; fail fast in Production |
§7 "Single-impl interface IIdentityProvider"; BL-006 |
S | Med | P1 | 2 | — | SIGN-OFF | done |
| RB-10 | ssp/auth + bhp/auth + ssp/shell | testability | Extract parseStoredSession (×2 apps) + spec redactProfile; assert a stored BSN yields '' |
§3a auth 42.9%/46.2% (worst FE line, §8); file LH 2/LF 20, BRH 3/BRF 13 | S | Low | P1 | 2 | — | SIGN-OFF | done |
| RB-11 | ssp/brief + libs/shared/infra | security | Dev hatches out of prod on the 3 hand-written fetch paths; export their parse boundaries; fix the doc |
§3b ssp/brief 42% reach (11/26, none ui/); §3a 68.8% branch |
M | Med | P1 | 2 | — | SIGN-OFF | done |
| RB-12 | backend/tests (CI) | security gate | One test enumerating the route table; every route hits an authz wrapper or an explicit allow-list | BL-006 (zero backend architecture enforcement) | M | Low | P1 | 3 | — | SIGN-OFF | done |
| RB-13 | ssp/auth + bhp/auth | ADR execution | Land Session → Principal; MedewerkerAdapter; backoffice login stops being a DigiD/BSN form |
BL-002 (211→151 dup after ADR-C-006; expected <40 after this) | M | Med | P1 | 3 | RB-09 | SIGN-OFF | done |
| RB-14 | repo (CI) | security gate | dotnet list package --vulnerable --include-transitive as a failing step |
BL-006; §7 (the .NET tree is entirely unscanned today) | S | Low | P2 | 3 | — | SIGN-OFF | done |
| RB-15 | backend/Program.cs | security | Wrap Swagger + the OpenAPI document in if (app.Environment.IsDevelopment()) |
BL-003; §3c Program.cs 97.4%/84.8% | S | Low | P2 | 3 | — | SIGN-OFF | done |
| RB-16 | backend/Stamdata | input valid. | DateOnly.TryParse on ?peildatum= → 400 instead of an unhandled 500 |
§3c Stamdata 96.8% line / 71.7% branch (BL-005) | S | Low | P2 | 3 | — | SIGN-OFF | done |
| RB-17 | libs/shared/app + brief + beheer | CQRS-light | Split runResult (fold) from runSubmit (fold + idempotency mint); point the 5 reads at it |
BL-007; §7 "read adapters 20 / mutations inline ~13" | S | Low | P2 | 3 | — | SIGN-OFF | done |
| RB-18 | backend/Data | security | Key IdempotencyStore on {SubjectId}:{idemKey} |
§7 stores "Not behind any port"; agent 02's Data note (no TTL, no reset) | S | Low | P2 | 3 | RB-17 | SIGN-OFF | done |
| RB-19 | backend/Program.cs | structure | Reorder all 48 endpoints under read/write sub-banners; regroup admin-cases + org-template preview | BL-003 (940 lines, file CC 78 vs next-highest 27) | S | High | P2 | 4 | RB-12 | SIGN-OFF | done |
| RB-20 | ssp/registratie | CQRS-light | ApplicationsStore.cancel / AdminCasesStore.delete through runSubmit; surface the error |
BL-007; §7 "Command factories 3" | S | Low | P2 | 4 | — | SIGN-OFF | done |
| RB-21 | ssp/registratie | CQRS-light | Extract the read half of createDraftSync into application/find-concept.ts |
§4a createDraftSync 143 lines — longest fn in the repo; §9 (>40) |
M | Med | P2 | 4 | — | — | done |
| RB-22 | ssp/brief | CQRS-light | (expand) BriefStore.load() tolerates a 404 by calling the existing reset() once |
BL-003; §7 Backend CQRS-light row | S | Low | P2 | 4 | — | SIGN-OFF | done |
| RB-23 | backend/Program.cs + Data | CQRS-light | (contract) GET /brief 404s when absent; GetOrCreate → Get |
BL-003; §7 Backend CQRS-light row | S | Med | P2 | 4 | RB-22 | SIGN-OFF | done |
| RB-24 | libs/shared/upload | ADR conform. | Move upload/ into infrastructure/domain/application; delete the depcruise carve-out |
BL-010; §7 "+1 adapter outside infrastructure/", "8 of 9 machines in domain/"; §3b shared/domain 0% reach |
M | Med | P2 | 5 | — | SIGN-OFF | done |
| RB-25 | libs/shared/upload | testability | UPLOAD_TRANSPORT injection token (the SESSION_PORT shape) instead of inject(KeepaliveTransport) |
§3a upload 52.0%/50.0%; §3b file unreached, non-ui/ |
S | Low | P2 | 5 | RB-24 | SIGN-OFF | done |
| RB-26 | libs/shared/upload | testability | Move the accept/reject decision to planFileSelection in upload.machine.ts |
§3a upload 52.0%/50.0%; §4a module max CC 27 | S | Low | P2 | 5 | RB-24 | SIGN-OFF | done |
| RB-27 | libs/shared/upload | testability | Extract uploadOutcome(status, responseText) out of the XHR closure |
file LH 5/64 (7.8% line), BRH 3/57 (5.3% branch) | S–M | Low | P2 | 5 | RB-25 | SIGN-OFF | open |
| RB-28 | libs/beheer + ssp/brief | testability | BLOB_PRESENTER token; the 3 commands' success paths become assertable |
§3a beheer/application 40.5% branch — worst FE; brief.store BRH 32/64 | S–M | Low | P2 | 5 | — | SIGN-OFF | done |
| RB-29 | backend/Domain | testability | Thread the existing at through LetterHtml.ResolveAuto instead of reading UtcNow |
§3c Domain 82.0% branch; §4b LetterHtml.cs CC 21 |
S | Low | P2 | 5 | — | — | done |
| RB-30 | backend/Data + Domain | testability | Extract 5 brief guards into Domain/Letters/BriefRules.cs; add tests/Domain/BriefRuleTests.cs |
§3c Data 75.5% branch (BL-005); §4b BriefStore.cs CC 17, ToDto CC 16 |
M | Med | P2 | 5 | — | SIGN-OFF | done |
| RB-31 | 4 app contexts (specs only) | ADR conform. | Replace hand-rolled state literals with given(reduce, initial) replays in 4 machine specs |
§7 Elm machines 9 (1 has a *.testing.ts); §3a herreg 67.8% / brief 68.8% branch |
M | Low | P2 | 6 | — | — | open |
| RB-32 | libs/shared/docs | ADR conform. | Add the missing language-switcher row to the CIBG gap register (9 markers vs 8 rows) |
§2 libs/shared 86 files / 5 194 lines; §6 layout Ca 22 | S | Low | P3 | 6 | — | — | open |
| RB-33 | libs/shared/testing | ADR conform. | Adopt unwrapOk at its one call site — or delete it; both satisfy ADR-0006 §3 |
BL-004; §3a libs/shared/testing 3 files, 100% line | S | Low | P3 | 6 | — | — | open |
Notes on the table
Why P1 is not simply "everything". Rule 2's P1 definition ("violates a correct ADR, blocks testability, or is a BIO2 compliance risk") would catch nearly every finding, which would make the score useless. It is applied as: P1 = a control is broken, an accepted ADR's decision is unexecuted, or a security-relevant guard has no test today. A ticket that is merely flagged because it touches a control (TE-003/4/5/6/8, CQ-006, ADR-C-002) is P2 with mandatory sign-off — the compliance risk is one the ticket could introduce, not one that exists. That distinction is the whole reason rule 4 is orthogonal to rule 2.
RB-01 and RB-02 sort above every structural ticket regardless of effort. Both are live
production-shaped defects, independently verified: a BSN concatenated into the persisted
authz audit Resource (Program.cs:674) and an unauthorized document-content endpoint
(GET /uploads/{documentId}/content). Four documents claim the audit trail holds no PII
and the test cited as enforcing it (AuthzAuditTests.cs:51-53) asserts on column
names, so the BSN travels in a column called Resource that the regex cannot see — the
value-asserting test is part of RB-02's definition of done, not a follow-up.
RB-11 ships the doc correction in the same diff as the code. ?role= and ?subject=
are not stripped from production builds on three hand-written fetch adapters, while
docs/reference/roles-and-access.md:23 says "they do not exist in a production build".
Correcting the doc without the code, or the code without the doc, both leave the repo
lying about itself. ?subject= additionally writes a BSN into sessionStorage in any
build, which is the specific thing SessionStore's G1 comment promises never happens.
RB-12 before RB-19, deliberately. Agent 07 flags CQ-006 as needing the authz suites as its safety net; agent 04 flags it as the prerequisite for OOM-A. RB-12's route-table test is the check that "each moved endpoint kept its gate" is verified by CI rather than by a reviewer's eye across a 900-line diff. RB-19 carries the only High risk in the table for exactly that reason and must land alone, never mixed with a behaviour change.
RB-07 gates ADR-C-009, not the other way round. Agent 06's proposed four-part test for runtime-editable config includes "writes are admin-capability-gated and audited". Today they are gated and not audited. Signing the ADR amendment first would ratify a control the code does not implement.
RB-13's dependency on RB-09 is real, not stylistic. Landing Principal on the
frontend alone closes ADR-C-004 and leaves BIO-002 wide open: a production behandelportal
build still resolves to the seeded zorgverlener — failing closed on backoffice
capabilities (correctly) but open on every citizen-scoped endpoint and holding
CanRevealBigNummer, because drafter is the no-header default. RB-09 makes "no
identity" representable at the interface; RB-13 is the FE half.
Merges — what was deduplicated, and how confident each merge is
| Merged ticket | Findings folded in | Confidence | Reasoning |
|---|---|---|---|
| RB-10 | TE-001 + BIO-017 | Certain | Agent 07 says outright: "this is TE-001 plus one assertion; it does not need its own ticket if TE-001 is scheduled". BIO-017's second half (redactProfile spec) is a five-line spec in the same PII-guard category, so it rides along. |
| RB-11 | BIO-012 + TE-002 + BIO-006(a) + BIO-006(b) | Certain | Agent 07 instructs: "Fix all three in one touch of the file, or the next reviewer will re-open it." All four land in the same three fetch adapters plus role.ts/subject.ts plus one doc line. BIO-006(b) is the same doc edit as BIO-012's. |
| RB-09 | BIO-001(a) + BIO-001(b) + BIO-002 | Certain | BIO-001's own remediation is (a) fail-fast + (b) "give Resolve a way to say no identity (see BIO-002)". BIO-002's root cause is the same non-nullable Resolve. One change, one file pair. |
| RB-17 | CQ-003 + CQ-005 | Certain | Agent 04: "Fix them in one ticket; they are listed separately only because the module scope requires it." One shared-file split, five call sites. |
| RB-14/12 | BIO-016 split into (a) and (b) | Certain | Two unrelated CI changes of different size and different value; the rest of BIO-016's "Absent" list is genuinely a production gate and stays on the checklist. |
| RB-08 | BIO-003, sequenced behind RB-07 | High | Routing through CasesAdmin gives BIO-003's missing audit row for free once RB-07 has moved auditing to the allow path. Shipping BIO-003 first would mean writing the audit call twice. It can ship standalone if RB-07 slips. |
| RB-18 | BIO-018, sequenced behind RB-17 | High | Agent 07: "Sequence CQ-003 before BIO-018 so the scoping change lands on a smaller call set." Not a merge, an ordering constraint. |
| RB-25/26/27 | TE-003/004/005, sequenced behind RB-24 | Judgement call | Agent 04 argued BL-010 must be resolved before anything is layered onto the upload folder, and RB-24 (ADR-C-002) is the ticket that resolves it. But the three seams are each independently shippable today, against the current paths. If RB-24 is deferred or rejected, unblock all three — the dependency is hygiene, not correctness. |
Merges considered and rejected:
- BIO-008 / BIO-009 / BIO-010 kept as three tickets (RB-02/04/05). They share a theme
("no BSN in any audit row, log line or persisted error field") and a shared acceptance
criterion (assert on values, e.g. no stored string matching
\d{9}). They were not merged because they sit in three modules with three different test suites, and BIO-010 is conditional onZgw:Enabled(off by default) which gives it a different risk profile. Three one-line fixes that each ship alone beat one cross-module sweep. If a reviewer prefers one ticket, merging them is defensible — this is the least settled call here. - CQ-002 not merged into BIO-007 (RB-07). They are the two halves of the same admin-mutation-observability gap, but one is FE error surfacing and the other is BE auditing. Agent 07 asked only that they "ship aware of each other". Cross-referenced, not merged.
SessionStorenot merged across the TE-001 / residual-auth-duplication overlap. Both touchsession.store.ts, but agent 06 is explicit that merging the two apps' session stores now would cement a citizen DigiD/BSN login as the backoffice's login — the exact outcome ADR-0002 §3 exists to prevent. RB-10 lands the same seam twice, on purpose. The duplication question reopens only after RB-13, on re-measurement.- ADR-C-004 not merged into BIO-002. Split into RB-09 (BE, S) → RB-13 (FE, M) instead, because a single ticket spanning both would not be independently deployable.
Tickets that were rejected and split (rule 3)
CQ-007 → RB-22 then RB-23. As filed, CQ-007 is the one finding agent 04 marked "no — FE+BE together": the FE must handle a 404 that the BE does not yet return. Shipping it as one ticket is a coordinated release. Split into the standard expand/contract pair:
- RB-22 (expand, FE).
BriefStore.load()tolerates a 404 by calling the existingreset()command once. Deploys against today's backend as a no-op — the BE never 404s, so the branch is dead on arrival and provably safe. - RB-23 (contract, BE).
GET /briefreturns 404 when no brief exists;BriefStore.GetOrCreatesplits intoGet+ the already-existingResetAndCreate. Deploys only once RB-22 is live.
Agent 07 rejected CQ-007's documentation-only alternative outright: "a non-idempotent GET must be visible in the code, not only in a ticket". That alternative is therefore not on the table.
No other ticket failed the single-deploy test. TE-001 lands in two apps but in one merge; RB-24 touches 30 dependents but is one atomic move; RB-19 is a 900-line diff but zero-semantic-change.
ADR-fix tickets — architect approval required before any dependent code ticket
None of these five is a code change. All five change what the repo's architecture documents claim. Three of them require a matching CLAUDE.md correction in the same diff (CLAUDE.md's own precedence rule: "the docs win — update this file").
| ID | ADR | What the amendment does | Gates / blocks | CLAUDE.md edit? | Effort | Compliance | Status |
|---|---|---|---|---|---|---|---|
| ADR-C-001 | 0001 | Rewrite the worked example against the real backend; drop the 2 discharged out-of-scope bullets (every path it names no longer exists) | nothing | no | S | — | done |
| ADR-C-003 | 0001 | State that the generated client is the wire contract post-codegen; scope contracts/ to codegen gaps |
any ticket deleting the 4 surviving contracts/*.dto.ts, or adding a hand-written DTO for a generated endpoint. No open ticket below is blocked today — recorded so a future one is. |
yes (§4) | S | — | done |
| ADR-C-007 | 0003 | Repoint 5 WP-67-stale paths; replace the factually false app-alert hand-rolled example (it wraps vendored .feedback classes) |
nothing | yes (§2) | S | — | done |
| ADR-C-009 | 0004 | Generalise "the org-templates exception" into a stated four-part test; list both passing surfaces | RB-07. Clause (4) is "writes are admin-capability-gated and audited". Today they are gated and not audited — sign this before RB-07 and the ADR ratifies a control the code does not implement. | yes (§4) | S | SIGN-OFF | done |
| ADR-C-005 | 0002 | (already landed — see "Already done") | was the gate on RB-13; now cleared | — | — | — | done |
No ADR-fix is proposed against ADR-0002 §3's non-sharing rule. Agent 06 considered it
as instructed and rejected it with evidence: grep -rn "Principal" apps libs returns one
comment and no type, so the rule was never tested, only unexecuted. Amending it now
would ratify the omission rather than the evidence. The correct sequence is
ADR-C-005 (done) → RB-13 → re-measure BL-002; agent 06's expectation is that the
residual duplication drops from 151 lines to under 40 on its own. If RB-13 is still
unstarted at the next backlog cycle, that is when the ADR-fix conversation becomes
legitimate.
Production gates — a release checklist, not tickets
These are correct for a POC and must be true before the system holds real BSNs. They are deliberately kept out of the ticket table: they are acceptance criteria for a release that does not exist yet (there is no production build artifact at all — BIO-020), not work that can be merged and deployed this week. Where a part of a production-gate finding was shippable now, that part was pulled out as a ticket and is named below.
Identity and access (9.1, 9.2, 9.4)
- Replace
StubIdentityProviderwith verified DigiD / employee-SSO claims.X-Role,X-Subject,X-Medewerker,X-Rollen,X-Adminremoved as inputs, not ignored. — BIO-001 - Verify by building both apps
--configuration productionthat the backoffice cannot act as a citizen. — BIO-002 (the interface half is RB-09; the FE half is RB-13) - Row-level scoping on every read returning person data; acceptance = a second seeded citizen cannot see the first's dashboard, notes, BRP address or diplomas. — BIO-013
- The PII-reveal capability comes from the app overlay, not the coarse role, and is
not held by the default role. — BIO-006 (the
X-Step-Upliteral is in RB-11) - Real step-up: a server-verified assurance/recency attribute no client can satisfy with a constant. — BIO-006
Cryptography (8.24)
- Encryption at rest with documented key custody and rotation. — BIO-014 Prerequisite: RB-02/04/05 first, so the BSN is not in three places that do not need it before deciding what must be encrypted.
- Document bytes move to encrypted object storage keyed by
DocumentId. — BIO-014 - TLS everywhere:
UseHttpsRedirection+ HSTS at the edge. — BIO-015 - Security response headers (
nosniff, CSP,Referrer-Policy) and a realAllowedHosts. — BIO-015 (the Swagger gate is RB-15)
Logging, monitoring and retention (8.15, 8.16)
- Audit retention, integrity and access defined — how long, append-only, and who may
read
/beheer/audit(it reusescases:manage, whichProgram.cs:565already flags as a placeholder for a dedicatedaudit:read). - Log shipping and alerting — the audit trail is a SQLite table with no export path.
- (Covered by tickets: allow-path auditing = RB-07; no BSN in any audit row, log line or persisted error field = RB-02/04/05.)
Data protection (5.12, 5.13)
- A DPIA covering BSN, uploaded identity documents and the register, with lawful basis and retention schedule. Nothing in the repo covers this.
- Deletion / retention policy for uploaded documents and the audit trail.
- (Covered: data minimisation on list endpoints = RB-03.)
Secure development (8.25, 8.28, 8.29)
- Secret scanning in CI (prevention — nothing is committed today, verified). — BIO-016
- Backend architecture enforcement (NetArchTest/ArchUnitNET) so
Domain/purity, ZGW containment (ADR-0005) and "authorization lives inAuthz" are CI- rather than review-maintained. — BL-006 - A coverage ratchet, so a security fix can be verified as not regressed by CI. — BL-009
- Penetration test / DAST, with BIO-004's object-level authorization and BIO-005's document linking as named cases.
- (Covered: backend dependency scanning = RB-14; the authorization regression gate = RB-12.)
Change control (8.32)
- A production build and deployment artifact exists, separate from the demo compose file, and its release checklist references this list. — BIO-020
- Verify by build, not by reading: in a production bundle
?role=,?subject=,?scenario=,?rollen=and the⚙ statepanel are all inert — including on the three hand-writtenfetchpaths. — BIO-012 (the code fix is RB-11; this box is the build-time proof)
Verified clean — do not "fix"
Each of these was read and judged correct by the agent named. Re-checking them is wasted effort; "simplifying" them is a regression.
Security and access control (agent 07, verified endpoint by endpoint)
AccessStore.can()deny-by-default +whenReady()— the pair exists so the guard cannot readcan()mid-load and deny an entitled user.capabilityGuard's "UX pre-gate, the backend re-enforces" claim — verified true for all six admin surfaces; every capability the guard checks has a server-side twin.Authz.CanBeoordelen's caller-kind derivation — the one capability a forgedX-Rolecannot reach, and the reason BIO-002 fails closed in that direction.- The four-eyes rule in
Authz.CanActOn, Forbidden-before-Conflict ordering. - The
isDevMode()gate on the debug panel and on the interceptor chain (the interceptor chain is correctly gated — RB-11 is about the three adapters that bypass it). - The ZGW client secret never reaching the browser; the notification webhook failing closed
on an unset secret;
ZgwDiagnosticHandlerlogging no bodies and being opt-in. - The upload content-type allow-list enforced server-side — which is also why
nosniffis a checklist item and not a finding. - Stamdata having no runtime write endpoint at all.
libs/shared/src/kernel/{bsn,pii}.ts— the standard the rest should be measured against.- No secrets committed; no
.dbfile tracked (both verified bygit check-ignore/ls-files).
Architecture and structure
- ADR-0005 is fully conformed — zero findings (agent 06). The ZGW anti-corruption layer is the repo's worked example; the ADR even predicted its own remaining gap and the gap stayed where predicted.
bhp/behandelingis the CQRS-light reference implementation (agent 04). Query adapters, command adapter and command factory in separate files, write-free read stores. Do not "clean it up".- The FE dependency structure is not a problem area (baseline §6): 0 violations across
11
severity: errorrules, textbook instability gradient (kernelI=5%, contexts I≥83%). Do not spend tickets here. BigProfileStore— the reference implementation of the read/write split (agent 04).- The
ToDetailDto(now)/ToDto(now)status projection — a real read-model derivation; do not let a future ticket "simplify" it into a stored status column (agent 04). - The 7 static backend stores and
[assembly: DisableTestParallelization]— deliberate, documented inData/Db.cs, and explicitly not challenged by agents 02, 04 or 07. RB-30 works because the rules never needed the DbContext, not by redesigning stores.
Baseline rows closed as false gaps (agent 02, verified — do not ticket them)
libs/shared/domain0% reach / 3 files, andlibs/beheer/contracts0% reach / 1 file. Both are pure type declarations with zero executable statements; 0% is correct and unimprovable. BL-004 named both as "genuine gaps"; that part of BL-004 is superseded.- 23 of the 25 CC>10 TS functions are reducers /
parse*/validate*— mandated house idioms (BL-001). A bare CC number is not grounds for a ticket against any of them. createDraftSyncis acquitted on testability (explicit deps object, optional injection,enabled()escape hatch, has a spec). RB-21 is a CQRS split, not a fix.httpClientFetch,Contracts/Mappers.cs,submit-besluit.ts,breadcrumb-trail.ts,route-focus.ts,AccessStore.can()— all "missing test, not blocked test", or a seam that costs more than it returns. Filing them would be volume, not quality.
Out of mandate — recorded so a later phase does not read this file as a step toward them
- OOM-A —
Program.cs→Features/folders with handler types. BL-003's most obvious invitation, and out of mandate because §7 is explicit that the backend has "no handler types, no mediator, noFeatures/folders" — there is no structure to extend, only one to introduce. RB-19 is a strict prerequisite if it is ever taken: you cannot cut a 940-line file into vertical slices while five of its seven sections interleave directions. Agent 03, which would have owned this, did not run. - OOM-B — read/write repository split in
backend/Data. Would introduce the pattern where §7 records it absent, and collides with the documented static/no-DI design. - OOM-C — no read model, no event sourcing, and none proposed.
- OOM-D — BL-011: the FE suite is flaky under parallel load, and BL-009 means nothing
ratchets. "CI green" alone does not verify any ticket in this backlog. Verify against
00-baseline.md's numbers — and note that §3a, §3b and §5 are already partly stale after the five shipped commits (auth duplication 211→151;libs/shared/infrastructurecoverage no longer dragged down by the generated client). Re-run the baseline before using it as the before-picture for any ticket below.
Provenance
| Source finding | Where it went |
|---|---|
| TE-001…008 | RB-10, RB-11, RB-25, RB-26, RB-27, RB-28, RB-29, RB-30 |
| TE-009 | shipped |
| CQ-001, 002, 003+005, 006, 007 | RB-21, RB-20, RB-17, RB-19, RB-22+RB-23 |
| CQ-004 | shipped (BE audit half outstanding → RB-07) |
| ADR-C-001, 003, 007, 009 | ADR-fix table |
| ADR-C-002, 004, 008, 010, 011 | RB-24, RB-13, RB-32, RB-31, RB-33 |
| ADR-C-005, 006 | shipped |
| BIO-001, 002 | RB-09 + checklist |
| BIO-003, 004, 005, 007, 008, 009, 010, 011, 018, 019 | RB-08, RB-01, RB-06, RB-07, RB-02, RB-04, RB-05, RB-03, RB-18, RB-16 |
| BIO-006 | RB-11 (a+b) + checklist (c) |
| BIO-012, 017 | RB-11, RB-10 |
| BIO-015, 016 | RB-15 + checklist; RB-14 + RB-12 + checklist |
| BIO-013, 014, 020 | checklist only |
| BL-008 | shipped |
| BL-005, BL-009, BL-011 | unowned — see "Coverage of this backlog" and OOM-D |
| BL-001, BL-002, BL-004 (partly), BL-006, BL-007, BL-010 | absorbed into the tickets/checklist above |