BriefStore.load() now treats a 404 from GET /brief as "no brief exists yet" and calls the existing reset() command once, instead of showing the generic load-failed error. BriefAdapter.load() gains a BriefLoadFailure error channel (notFound | error) so the store can tell a 404 apart from every other failure; every other adapter method stays on runSubmit, unchanged. The once-only bound is a field on the store, not a comment: a second 404 (from a later load() call) always falls through to the ordinary error path, and the recovery path never calls load() again, so no loop can form. This is the expand half of CQ-007's split (04-cqrs-light.md). Today's backend never 404s GET /brief, so the new branch is dead code until RB-23 (the backend contract half) ships in a later merge. 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** | open |
|
||
| **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** | open |
|
||
| **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** | open |
|
||
| **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 | — | — | open |
|
||
| **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** | **implemented** |
|
||
| **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** | open |
|
||
| **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** | open |
|
||
| **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** | open |
|
||
| **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** | open |
|
||
| **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** | open |
|
||
| **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 | — | — | open |
|
||
| **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** | open |
|
||
| **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 | — | pending |
|
||
| **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 | — | pending |
|
||
| **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 | — | pending |
|
||
| **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** | pending |
|
||
| **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 |
|