UploadAdapter.xhrUpload built new XMLHttpRequest() directly and put the
actual decisions inside its load listener: 2xx-vs-not, JSON.parse of the
body with a fallback, and ProblemDetails mapping via parseError. None of
it was reachable without stubbing the XHR global, so it had no spec
(TE-005; file LH 5/64, BRH 3/57).
Extract uploadOutcome(status, responseText): Result<string, {
documentId }>, a pure function next to genericError/parseError. It holds
the 2xx check, the JSON.parse-with-fallback, and the ProblemDetails
mapping. The load listener is now a two-line dispatch into it.
Abort-vs-error disambiguation stays where it is: it decides whether a
response exists at all, before uploadOutcome would even run, and the
proposed signature has no field for "aborted". It is already a one-line
ternary with no DOM-only logic to extract.
Add upload.adapter.spec.ts: plain describe/it, no DOM, no XHR stub,
covering a 2xx success, a 2xx unparseable body, a non-2xx ProblemDetails
body, a non-2xx non-ProblemDetails body, and the 200/300 boundary.
Verified red by editing uploadOutcome down to one line (an Edit, not
git checkout): 4 of 5 new specs failed. Re-applied with a second Edit.
Coverage for upload.adapter.ts: LH 5/64 -> 12/65, BRH 3/57 -> 7/59.
Skip TE-005's optional half (moving the currentScenario() branch into
KeepaliveTransport.send()): it needs a second file, upload-shell.
service.ts, and this ticket's own scope fences it to upload.adapter.ts
and its spec. The dev simulator's behaviour is unchanged.
Mark RB-27 implemented in 99-backlog.md and add its implementation note,
including a batch 5 close-out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
438 lines
47 KiB
Markdown
438 lines
47 KiB
Markdown
## 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, so `StubIdentityProvider` answers 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 in `implementation/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) and `rich-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 in `OpenZaakZaakSourceTests.cs`; and the six files over 400 lines
|
||
(§9). RB-19 reorders `Program.cs` but 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; `Contracts` 65.0%, `Stamdata` 71.7%, `Data`
|
||
75.5% — `backend/tests/` has no `Contracts/` 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** | **implemented** |
|
||
| **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 on `Zgw: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.
|
||
- **`SessionStore` not merged across the TE-001 / residual-auth-duplication overlap.**
|
||
Both touch `session.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:
|
||
|
||
1. **RB-22 (expand, FE).** `BriefStore.load()` tolerates a 404 by calling the existing
|
||
`reset()` 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.
|
||
2. **RB-23 (contract, BE).** `GET /brief` returns 404 when no brief exists;
|
||
`BriefStore.GetOrCreate` splits into `Get` + the already-existing `ResetAndCreate`.
|
||
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 `StubIdentityProvider` with verified DigiD / employee-SSO claims. `X-Role`,
|
||
`X-Subject`, `X-Medewerker`, `X-Rollen`, `X-Admin` removed as **inputs**, not ignored. — BIO-001
|
||
- [ ] Verify by building both apps `--configuration production` that 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-Up` literal 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 real
|
||
`AllowedHosts`. — 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 reuses `cases:manage`, which `Program.cs:565` already flags
|
||
as a placeholder for a dedicated `audit: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 in `Authz`" 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 `⚙ state` panel are all inert — including on the
|
||
three hand-written `fetch` paths. — 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
|
||
read `can()` 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 forged `X-Role`
|
||
cannot 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; `ZgwDiagnosticHandler` logging no bodies and being opt-in.
|
||
- The upload content-type allow-list enforced **server-side** — which is also why
|
||
`nosniff` is 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 `.db` file tracked (both verified by `git 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/behandeling` is 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: error` rules, textbook instability gradient (`kernel` I=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 in `Data/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/domain` 0% reach / 3 files, and `libs/beheer/contracts` 0% 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.
|
||
- `createDraftSync` is **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, no `Features/` 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/infrastructure`
|
||
coverage 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 |
|