docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every WP done) and `docs/project/refactor-backlog-setup/` (the arc before it). Move both under `docs/project/archive/` with `git mv`, so history stays intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them, because it points at the now-archived backlog README. Add `docs/project/archive/README.md`. It states that these trees are historical and names the two directories that are still live. Repoint every inbound reference named in RD-30's Files table: CLAUDE.md, the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the `document-feature` and `new-ssp` skills, and the readable-codebase PLAN, README, and RD-19 ticket. Fix two upward-relative links inside the moved WP files (WP-68, WP-69) that gained a directory level and would otherwise break. Repoint `.prettierignore`'s two agent-prompt exclusions to their new path, so prettier keeps leaving those files' exact wording alone. Mark RD-30 done and check off its acceptance criteria; flip its README row to done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
## Scope: apps/ssp (auth, registratie, herregistratie, brief, showcase+shell+root), apps/behandelportal (auth, behandeling, shell+root), libs/shared (per layer), libs/beheer, backend (Program.cs, Domain, Data, Zgw, Contracts, Stamdata)
|
||||
|
||||
## Status: complete
|
||||
|
||||
## Last updated: 2026-08-26
|
||||
|
||||
## Depends on: 00-baseline.md
|
||||
|
||||
## ---
|
||||
|
||||
# 04 — CQRS-light: command/query separation at the application-service level
|
||||
|
||||
**Mandate reminder, applied literally.** This agent may only _extend_ CQRS-light where
|
||||
baseline §7 records it already exists. It may not introduce it. Every finding below names
|
||||
the concrete existing artifact it extends. Three things the baseline flagged as tempting
|
||||
are therefore **not** filed as tickets — they are in "Out of mandate (pattern absent)" at
|
||||
the end.
|
||||
|
||||
**What "the pattern" concretely is in this repo** (from §7 + CLAUDE.md §3, so later
|
||||
sections can just point at it):
|
||||
|
||||
| Side | The existing artifact |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| FE query | `resource({ loader })` in an `infrastructure/*.adapter.ts` + a `parse*` boundary + a `providedIn:'root'` read store exposing `RemoteData` |
|
||||
| FE command | `application/submit-*.ts` command factory → `runSubmit(fn, fallback)` → `Result<string,T>` → the caller dispatches a Msg |
|
||||
| FE fold | `libs/shared/src/application/submit.ts` — the single try/catch + ProblemDetails → error-string fold, **and** the Idempotency-Key mint point |
|
||||
| BE | `Contracts/Dtos.cs` direction split (`*Request` in / `*Dto`+`*Response` out); read/write comment banners in `Program.cs` |
|
||||
| BE read-mdl | `ToDetailDto(now)` / `ToDto(now)` — the read side _projects_ status from timestamps rather than the write side storing it |
|
||||
|
||||
**The cleanest module in the repo is `bhp/behandeling`**, and it is worth naming up front
|
||||
because three findings below propose making another module look like it: it splits its
|
||||
query adapter (`beoordeling.adapter.ts` `get`, `werkvoorraad.adapter.ts` `list`) from its
|
||||
command adapter (`besluit.adapter.ts` `besluit`) into **separate files**, wraps only the
|
||||
command in a command factory (`application/submit-besluit.ts`), and keeps the read store
|
||||
(`beoordeling.store.ts`, `werkvoorraad.store.ts`) write-free. Its backend counterpart does
|
||||
the same: `Program.cs:441` "read side only" banner, `Program.cs:464` the write banner. That
|
||||
is the target shape, and it is already in the tree.
|
||||
|
||||
---
|
||||
|
||||
## apps/ssp — auth
|
||||
|
||||
**No findings.**
|
||||
|
||||
`SessionStore.login` (`apps/ssp/src/app/auth/application/session.store.ts:58`) is a write
|
||||
and `session`/`isAuthenticated` are reads, but they share one three-field aggregate with no
|
||||
network read path at all — `DigidAdapter.authenticate` is the only I/O and it is a command.
|
||||
There is nothing to separate. §7 lists no command factory or read adapter in this context to
|
||||
extend. BL-002 is agent 06's, not this agent's.
|
||||
|
||||
---
|
||||
|
||||
## apps/ssp — registratie
|
||||
|
||||
### CQ-001 — `createDraftSync` is registered as a command factory but owns three query paths
|
||||
|
||||
- **Module / file:line** — `apps/ssp/src/app/registratie/application/draft-sync.ts:50-236`
|
||||
(queries at `:141 load`, `:160 findConcept`, `:179 resume`; commands at `:63 ensureId`,
|
||||
`:100 flush`, `:213 submit`, `:221 reset`)
|
||||
- **Extends** — the command-factory idiom itself. §7 counts `draft-sync.ts` as one of the
|
||||
repo's **3 command factories**, alongside `submit-change-request.ts` (18 lines, one
|
||||
command, zero reads) and `submit-besluit.ts` (16 lines, one command, zero reads). This
|
||||
finding asks the third member of that set to look like the other two.
|
||||
- **Baseline citation** — §7 Frontend, "Command factories (write side) | 3"; §4a metric
|
||||
row **`createDraftSync` 143 lines** (the largest function in the codebase, tied to
|
||||
`reduceUpload`'s 109 only in the two-member `fn>75` population); §9 threshold "TS
|
||||
function > 40 lines".
|
||||
- **The mixing, concretely** — the factory returns four members. `resume()` is pure query
|
||||
orchestration: read `?aanvraag`, `adapter.detail(linked)`, else `findConcept()` →
|
||||
`adapter.list()` → `parseApplications`. `submit()`/`reset()`/the debounce `effect` are
|
||||
writes. They are entangled through three pieces of shared mutable closure state — `id`,
|
||||
`ensuring`, and `resumeGate` (`:56-61`) — where `resumeGate` exists _only_ so the write
|
||||
path (`ensureId`) can wait for the read path (`resume`) to finish. That coupling is
|
||||
genuine and load-bearing, which is exactly why it is worth naming rather than leaving as
|
||||
an unexplained 143-line function.
|
||||
- **Proposed change, minimal** — extract the read half into
|
||||
`application/find-concept.ts`: `findConcept(adapter, type)` and `loadConcept(adapter, id)`
|
||||
as free functions taking the adapter (no `inject`, so they get a direct spec —
|
||||
`draft-sync.spec.ts` already exists and would shrink). `createDraftSync` keeps `resumeGate`
|
||||
and the write path and calls them. This is a move, not a redesign; the closure state stays
|
||||
where it is.
|
||||
- **Effort** — M. Independently shippable in one deploy (no wire change, no DTO change).
|
||||
|
||||
### CQ-002 — two read stores perform writes that bypass the `runSubmit` fold
|
||||
|
||||
- **Module / file:line** — `apps/ssp/src/app/registratie/application/applications.store.ts:54-64`
|
||||
(`cancel`) and `apps/ssp/src/app/registratie/application/admin-cases.store.ts:46-56`
|
||||
(`delete`); the adapter methods are
|
||||
`infrastructure/applications.adapter.ts:60 cancel` and `:41 deleteAny`.
|
||||
- **Extends** — `runSubmit` + `SUBMIT_FAILED` (`libs/shared/src/application/submit.ts:15,28`),
|
||||
the fold that all 16 other mutations in the repo pass through, and the
|
||||
`createSubmitChangeRequest` command factory that lives _in this same folder_
|
||||
(`registratie/application/submit-change-request.ts`) and does exactly this for the other
|
||||
registratie write.
|
||||
- **Baseline citation** — **BL-007** ("the FE write side is inconsistently placed");
|
||||
§7 Frontend, "Command factories | 3" vs "Mutations living inline in adapters | ~13".
|
||||
Note these two are a _fourteenth and fifteenth_ case BL-007 did not enumerate: they are
|
||||
worse than the ~13, because those at least reach `runSubmit` inside the adapter — these
|
||||
reach the raw `ApiClient` and never produce a `Result` at all.
|
||||
- **The mixing, concretely** — both stores own a `RemoteData` read signal _and_ a write, and
|
||||
the write's failure path is `catch { this.state.set(before); }` — a bare rollback with no
|
||||
error channel. A failed cancel makes the row silently reappear with no message, no
|
||||
`ActionState`, no ProblemDetails `detail`. `BriefStore`/`OrgTemplateStore` in the sibling
|
||||
context both hold an `ActionState` + `lastError` for exactly this. The bare
|
||||
`adapter.cancel()` also means the `Idempotency-Key` on the wire is a fresh UUID minted per
|
||||
HTTP attempt by `api-client.provider.ts:58`, not the per-logical-submit key `runSubmit`
|
||||
promises at `submit.ts:11-13` — that invariant's docstring is currently false for these
|
||||
two calls (harmless today: `Program.cs` only honours the header inside the `Submit` helper,
|
||||
see CQ-005's note).
|
||||
- **Proposed change, minimal** — route both through `runSubmit` and surface the error.
|
||||
Two options, pick one and apply to both stores identically:
|
||||
(a) smallest — `const r = await runSubmit(() => this.adapter.cancel(id), SUBMIT_FAILED);
|
||||
if (!r.ok) { this.state.set(before); this.error.set(r.error); }` plus one `error` signal;
|
||||
(b) fuller — a `application/cancel-application.ts` command factory mirroring
|
||||
`submit-change-request.ts`, which the store injects. (a) is enough to close the finding.
|
||||
- **Effort** — S. Independently shippable; (a) touches 2 files plus a UI line each to render
|
||||
the error.
|
||||
|
||||
### Not filed — `applications.adapter.ts` mixes 3 reads and 5 writes in one file
|
||||
|
||||
`infrastructure/applications.adapter.ts:31-66` holds `list`/`listAll`/`detail` next to
|
||||
`create`/`syncDraft`/`cancel`/`deleteAny`/`submit`, where `bhp/behandeling` splits the
|
||||
equivalent into `beoordeling.adapter.ts` + `besluit.adapter.ts`. The split would be the
|
||||
structural enabler for CQ-002(b). On its own, though, it moves 8 thin one-line
|
||||
`this.client.x()` wrappers between files and changes nothing observable — file placement is
|
||||
agent 03's axis, not a read/write-mixing defect. Noted here so it is a deliberate omission
|
||||
rather than a miss; fold it into CQ-002 if that ticket takes option (b).
|
||||
|
||||
### Not filed — `BigProfileStore`
|
||||
|
||||
`application/big-profile.store.ts` is the reference implementation of the split and needs no
|
||||
change: reads are two resources projected through `parseDashboardView`, and the only
|
||||
write-adjacent members are the `beginHerregistratie`/`confirmHerregistratie`/
|
||||
`rollbackHerregistratie` invalidation hooks (`:65-74`) — the store never performs the write
|
||||
itself, `createDraftSync.submit` does. ADR-0001's "Out of scope" section already records the
|
||||
optimistic-flag race; that is a correctness note, not a CQRS-light one.
|
||||
|
||||
---
|
||||
|
||||
## apps/ssp — herregistratie
|
||||
|
||||
**No findings.**
|
||||
|
||||
`IntakePolicyStore` (`application/intake-policy.store.ts`) is a pure query facade over one
|
||||
`resource()`. The context has no write of its own — its submit is `createDraftSync.submit`,
|
||||
owned by `registratie` and covered by CQ-001. `intake.machine.ts` / `herregistratie.machine.ts`
|
||||
are reducers; §3's "side effects stay out of the reducer" is honoured (verified: no `inject`,
|
||||
no adapter import in either).
|
||||
|
||||
---
|
||||
|
||||
## apps/ssp — brief
|
||||
|
||||
### CQ-003 — `runSubmit` (the write-side fold, incl. the Idempotency-Key mint) is used for reads
|
||||
|
||||
- **Module / file:line** — `apps/ssp/src/app/brief/infrastructure/brief.adapter.ts:56`
|
||||
(`load` → `briefGET`), `apps/ssp/src/app/brief/infrastructure/org-template.adapter.ts:39`
|
||||
(`list` → `orgTemplates`) and `:51` (`load` → `orgTemplateGET`). Same defect in
|
||||
libs/beheer — see that section; one ticket should fix all five call sites.
|
||||
- **Extends** — the two halves already present in `libs/shared/src/application/submit.ts`:
|
||||
the ProblemDetails→`Result` **fold** (which reads legitimately want) and the
|
||||
**`withIdempotencyKey` wrapper** (which is write-only by construction). The read idiom it
|
||||
should join is the one the other 6 read adapters use — `resource({ loader })` +
|
||||
`parse*` — or, for these imperative reads, the fold alone.
|
||||
- **Baseline citation** — **BL-007**; §7 Frontend "Infrastructure adapters (read side) | 20"
|
||||
and "Mutations living inline in adapters | ~13" — these three GETs are counted in the
|
||||
wrong column of that inventory, because the code cannot tell them apart from the writes
|
||||
beside them.
|
||||
- **The mixing, concretely** — `submit.ts:11-13` states `runSubmit` is "the one place a
|
||||
logical submit's Idempotency-Key is minted — once per `runSubmit` call". Each of these
|
||||
reads therefore mints a UUID and assigns the module-level `pendingIdempotencyKey`
|
||||
(`api-client.provider.ts:21-26`) for the duration of a GET. It is inert today: the header
|
||||
is attached only when `method !== 'GET'` (`api-client.provider.ts:58`). But
|
||||
`api-client.provider.ts:15-19` explicitly documents that the module-level variable "holds
|
||||
up because every submit command calls its adapter synchronously (no await before reaching
|
||||
this file)". Reads have no such discipline — `BriefStore.load()` and
|
||||
`OrgTemplateStore.load()` are awaited across `await`s — so routing reads through the write
|
||||
helper quietly widens the assumption that comment relies on. Reading `brief.adapter.ts:55-92`
|
||||
it is also simply impossible to see which of the seven methods are commands: all seven are
|
||||
`await runSubmit(...) → parseBriefView`.
|
||||
- **Proposed change, minimal** — split `submit.ts` in place, no new concept:
|
||||
`runResult(fn, fallback)` = the existing try/catch + `problemDetail` fold;
|
||||
`runSubmit(fn, fallback)` = `runResult(() => withIdempotencyKey(crypto.randomUUID(), fn), fallback)`.
|
||||
Point the 5 reads at `runResult`. Zero behaviour change, and afterwards the keyword at each
|
||||
call site states the side. `submit.spec.ts` already exists and covers the fold.
|
||||
- **Effort** — S. Independently shippable in one deploy; 1 shared file + 3 adapters (+2 in
|
||||
libs/beheer).
|
||||
|
||||
### Not filed — `BriefStore` and `OrgTemplateStore` own both the read path and the write commands
|
||||
|
||||
`application/brief.store.ts` holds `load()` alongside `save`/`submit`/`approve`/`reject`/
|
||||
`send`/`resetDemo`/`revealBigNummer`; `application/org-template.store.ts` holds
|
||||
`load`/`selectSubOrg` alongside `flushSave`/`confirmPublish`/`rollback`. BL-007 points at
|
||||
these as "mutations living inline in adapters", and it is tempting to file them.
|
||||
|
||||
They are deliberately not filed, and the reason matters for whoever reads this next.
|
||||
CLAUDE.md §3 defines a command as "does the HTTP, then dispatches a message describing the
|
||||
outcome" — and that is precisely what `BriefStore.transition()` (`:248-259`) and
|
||||
`OrgTemplateStore.confirmPublish()` (`:175-189`) do: `ActionState → Busy`, cancel the
|
||||
debounce, call the adapter, then `store.dispatch(...)` or `actionState.set(Failed)`. The
|
||||
reducer stays pure. These stores _are_ the command layer; they are not a store that
|
||||
accidentally grew writes. Both also implement the write→invalidate-read handoff correctly
|
||||
(`confirmPublish` reloads via `selectSubOrg`, mirroring `BigProfileStore.confirmHerregistratie`'s
|
||||
`viewRes.reload()`). Extracting six `createSubmitX()` factories out of `BriefStore` would move
|
||||
code without changing which layer performs which effect. **The real defect in these two files
|
||||
is CQ-003, and that is filed.**
|
||||
|
||||
---
|
||||
|
||||
## apps/ssp — showcase, shell, root
|
||||
|
||||
**No findings.** `showcase/concepts.page.ts`, `app.ts`, `app.config.ts`, `app.routes.ts`
|
||||
contain no application services, no adapter calls and no state writes — routing, providers
|
||||
and a teaching page. §7 lists no pattern here to extend. (Their 0% spec reach in §3b is
|
||||
agent 02's; their `bhp/root` duplication in §5 is agent 01's.)
|
||||
|
||||
---
|
||||
|
||||
## apps/behandelportal — auth
|
||||
|
||||
**No findings.** Identical to ssp/auth by BL-002; the same reasoning applies.
|
||||
|
||||
---
|
||||
|
||||
## apps/behandelportal — behandeling
|
||||
|
||||
**No findings — this module is the reference implementation.**
|
||||
|
||||
Stated positively so later phases do not "clean it up" into something worse:
|
||||
`werkvoorraad.adapter.ts` (`list`) and `beoordeling.adapter.ts` (`get`) are query-only files;
|
||||
`besluit.adapter.ts` (`besluit`) is a command-only file; `submit-besluit.ts` is the command
|
||||
factory; `werkvoorraad.store.ts` and `beoordeling.store.ts` contain no writes at all — not
|
||||
even a rollback. `besluit-form.component.ts:88` holds the command
|
||||
(`private submit = createSubmitBesluit()`), never the adapter. §3a records the highest FE
|
||||
line coverage of any feature context here (91.6%), which is consistent with the split: the
|
||||
read stores are trivially testable because nothing writes through them.
|
||||
|
||||
---
|
||||
|
||||
## apps/behandelportal — shell, root
|
||||
|
||||
**No findings.** Same as ssp/shell+root.
|
||||
|
||||
---
|
||||
|
||||
## libs/shared — application
|
||||
|
||||
### CQ-004 — `FeatureFlagStore.set` writes without the fold and drops the error entirely
|
||||
|
||||
- **Module / file:line** — `libs/shared/src/application/feature-flags.store.ts:53-59`;
|
||||
adapter at `libs/shared/src/infrastructure/feature-flags.adapter.ts:18`; caller at
|
||||
`libs/beheer/src/ui/feature-flags.page.ts:93`.
|
||||
- **Extends** — `runSubmit`/`SUBMIT_FAILED` (`libs/shared/src/application/submit.ts`), which
|
||||
lives in this very folder — the store sits three files away from the fold it skips.
|
||||
- **Baseline citation** — **BL-007**; §7 Frontend "Mutations living inline in adapters | ~13"
|
||||
(this is another case not in BL-007's enumeration, which named only `brief.adapter.ts`,
|
||||
`org-template.adapter.ts` and `stamdata.adapter.ts`).
|
||||
- **The mixing, concretely** — the store owns the read (`load`, `flags`, `all`, `enabled`)
|
||||
and the admin write. The write is `try { await this.adapter.set(...) } finally { await this.load() }`
|
||||
— **no `catch`**. The rejection propagates out of `set()`; the caller is
|
||||
`void this.store.set(key, enabled)` (`feature-flags.page.ts:93`), so a failed toggle
|
||||
becomes an unhandled promise rejection. The admin sees the switch flick back after the
|
||||
`finally`'s reload with no explanation and no ProblemDetails `detail`, on a write gated by
|
||||
`flags:manage` that is exactly the kind an operator needs confirmation of. Every other
|
||||
write in the repo that goes through `runSubmit` gets `problemDetail(e, fallback)`.
|
||||
- **Proposed change, minimal** — `const r = await runSubmit(() => this.adapter.set(key, enabled), SUBMIT_FAILED);
|
||||
await this.load(); if (!r.ok) this.error.set(r.error);` with one `error` signal rendered by
|
||||
`feature-flags.page.ts`. Keep the reload unconditional (it is the read-side invalidation and
|
||||
is correct).
|
||||
- **Effort** — S. Independently shippable; 2 files.
|
||||
|
||||
### Not filed — the rest of the layer
|
||||
|
||||
`access.store.ts` (query-only over `/me`), `remote-data.ts`, `store.ts`, `action-state.ts`,
|
||||
`debounced-save.ts`, `history.ts`, `machine-remote-data.ts`, `pending-saves.ts`,
|
||||
`session.port.ts` are the read/state kit itself, not services. `submit.ts` is the subject of
|
||||
CQ-003 rather than a finding of its own.
|
||||
|
||||
---
|
||||
|
||||
## libs/shared — infrastructure
|
||||
|
||||
**No findings.** `me.adapter.ts` and `feature-flags.adapter.ts` are correctly
|
||||
direction-labelled (`list` vs `set`); `api-client.provider.ts` is the single HTTP seam and
|
||||
already gates the Idempotency-Key on `method !== 'GET'`, i.e. the _transport_ layer honours
|
||||
the command/query split that CQ-003 shows the _application_ layer blurring. The CC-19 `fetch`
|
||||
there is BL-001's "outside the idiom" case and belongs to agent 01.
|
||||
|
||||
---
|
||||
|
||||
## libs/shared — upload
|
||||
|
||||
**No findings.**
|
||||
|
||||
`upload.adapter.ts` mixes reads (`categoriesResource`, `status`) and writes (`xhrUpload`,
|
||||
`deleteDocument`), and `upload-shell.service.ts` mixes `upload`/`delete`/`cancel` (commands)
|
||||
with `pollReturning` (a query). It is tempting to file, and it is deliberately not: **BL-010**
|
||||
records that this whole folder sits outside the layer convention by design and is carved out
|
||||
by name in the `apiclient-infrastructure-only` dependency-cruiser rule. §7 lists no adapter
|
||||
or command factory here to extend — `upload.adapter.ts` is explicitly footnoted as the
|
||||
adapter that is _not_ in an `infrastructure/` folder. Resolving BL-010 (agent 03's call) has
|
||||
to come first; a read/write split layered on top of an already-exceptional layout would
|
||||
entrench the exception. Worth noting for whoever takes BL-010: `UploadShellService` is
|
||||
otherwise the FE's most complete command implementation — every method takes a `Dispatch` and
|
||||
reports its outcome as a Msg, which is the CLAUDE.md §3 shape done exactly right.
|
||||
|
||||
---
|
||||
|
||||
## libs/shared — domain, contracts, kernel, ui, layout, testing, environments
|
||||
|
||||
**No findings.** No application services, no adapters, no writes. `kernel/fp.ts`'s `Result`
|
||||
is the return type the command side is built on, not a service.
|
||||
|
||||
---
|
||||
|
||||
## libs/beheer
|
||||
|
||||
### CQ-005 — both stamdata reads run through `runSubmit`, in a file that documents itself as write-free
|
||||
|
||||
- **Module / file:line** — `libs/beheer/src/infrastructure/stamdata.adapter.ts:27` (`list`)
|
||||
and `:42` (`load`).
|
||||
- **Extends** — the same `submit.ts` split proposed in CQ-003. **Fix them in one ticket**;
|
||||
they are listed separately only because the module scope requires it.
|
||||
- **Baseline citation** — **BL-007** (which names `stamdata.adapter.ts` explicitly among the
|
||||
~13 "mutations living inline in adapters"); §7 Frontend "Infrastructure adapters (read
|
||||
side) | 20".
|
||||
- **The mixing, concretely** — sharper here than anywhere else in the repo, because the
|
||||
file's own docstring (`:16-20`) says: _"Both endpoints are reads … There is no write method
|
||||
— the edit is downloaded and lands as a PR."_ Both nevertheless call `runSubmit`. BL-007
|
||||
counts this adapter on the write side of the inventory on the strength of that call, when
|
||||
the module is in fact the repo's only genuinely CQRS-clean context: `StamdataStore` has no
|
||||
write command at all (`download()` at `:137` is a local `Blob` + anchor click, zero
|
||||
network), and `AuditStore` is read-only. The tooling and the baseline both mis-classify
|
||||
this module purely because of the helper name.
|
||||
- **Proposed change, minimal** — point both at `runResult` per CQ-003. Nothing else in this
|
||||
library changes.
|
||||
- **Effort** — S. Ships with CQ-003 in the same deploy.
|
||||
|
||||
### Not filed — `libs/beheer` application/domain/ui/contracts otherwise
|
||||
|
||||
`stamdata.store.ts` and `audit.store.ts` are query-only (see above). `stamdata-editor.machine.ts`
|
||||
is a pure reducer. The `ui/` layer holds no adapter calls.
|
||||
|
||||
---
|
||||
|
||||
## backend — Program.cs
|
||||
|
||||
### CQ-006 — the read/write banner split is established, honoured once, then abandoned for 5 of 7 feature sections
|
||||
|
||||
- **Module / file:line** — `backend/src/BigRegister.Api/Program.cs`. Banners at `:133`
|
||||
("GET: screen-shaped reads"), `:185` ("POST: submits"), `:441` ("read side only") and
|
||||
`:464` ("record a behandelaar's decision"). Mixed sections: `:197` Document upload,
|
||||
`:275` Applications, `:598` Brief, `:721` Organization templates, and admin-cases split
|
||||
non-contiguously across `:424`, `:554`, `:566`.
|
||||
- **Extends** — the banner convention _inside this file_, specifically the WP-65 pair at
|
||||
`:441`/`:464`, which already splits one feature's query endpoints from its command endpoint
|
||||
under two banners. This proposal applies the `:441`/`:464` treatment to the five sections
|
||||
that predate it. **No handler types, no mediator, no `Features/` folders** — see the
|
||||
out-of-mandate section for why that larger move is not proposed here.
|
||||
- **Baseline citation** — **BL-003** (940 lines, 48 endpoints, file CC 78 vs next-highest 27,
|
||||
"read/write separated only by comment banner"); §7 Backend CQRS-light row ("Read/write
|
||||
split exists as _comment banners_"); §3c `Program.cs` 97.4% line / 84.8% branch.
|
||||
- **The mixing, concretely** — the file opens by declaring direction as its organising
|
||||
principle (`:133` reads, `:185` writes), then from `:197` switches to feature grouping
|
||||
without saying so, and every subsequent section interleaves:
|
||||
| Section | Line | Reads | Writes |
|
||||
| -------------------------------------------------------------------------------------------- | ------------: | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Document upload | 197 | `GET /uploads/categories`, `GET /uploads/{id}/content`, `GET /uploads/status` | `POST /uploads`, `DELETE /uploads/{id}`, `DELETE /admin/uploads/{id}` |
|
||||
| Applications | 275 | `GET /applications`, `GET /applications/{id}` | `POST`, `PUT /{id}`, `DELETE /{id}`, `POST /{id}/submit` |
|
||||
| Admin cases | 424, 554, 566 | `GET /admin/cases`, `GET /admin/audit` | `DELETE /admin/cases/{id}` — **129 lines away** from its list, with werkvoorraad, beoordeling, besluit and the ZGW notification hook in between |
|
||||
| Brief | 598 | `GET /brief`, `GET /brief/preview` | `PUT /brief`, `POST submit/approve/reject/send/reveal-bignummer/reset` |
|
||||
| Org templates | 721 | `GET /admin/org-templates`, `GET /admin/org-template/{id}` | `PUT /{id}`, `POST /{id}/publish`, `POST /{id}/rollback` |
|
||||
| `GET /admin/org-template/{subOrgId}/preview` (`:703`) is additionally filed under the Brief |
|
||||
| banner rather than the Org-templates one. The consequence is not a bug — §3c confirms the |
|
||||
| file is well tested, and this is a **structure finding, not a correctness one** — it is that |
|
||||
| a reader cannot answer "what can mutate state here?" without reading all 940 lines, and that |
|
||||
| the two cross-cutting write wrappers (`Submit`'s idempotency replay, `RecordZgwDivergence`) |
|
||||
| have no visible scope. |
|
||||
- **Proposed change, minimal** — within each existing feature section, order reads first then
|
||||
writes and insert the `:441`/`:464`-style sub-banners; move `DELETE /admin/cases/{id}`
|
||||
(`:554`) and `GET /admin/audit` (`:566`) up beside `GET /admin/cases` (`:424`); move the
|
||||
org-template preview from the Brief section to the org-template one. **Pure reordering and
|
||||
comments** — no signature, route, DTO or behaviour change, so §3c's 97.4%/84.8% coverage is
|
||||
the regression net and the diff is reviewable line-for-line.
|
||||
- **Effort** — S. Independently shippable in one deploy. One caveat for whoever schedules it:
|
||||
it is a large-diff/zero-semantic-change commit, so land it alone, never mixed with a
|
||||
behaviour change.
|
||||
|
||||
### CQ-007 — `GET /brief` creates a brief, though the explicit create command already exists
|
||||
|
||||
- **Module / file:line** — `backend/src/BigRegister.Api/Program.cs:603`
|
||||
(`api.MapGet("/brief", …)`) → `backend/src/BigRegister.Api/Data/BriefStore.cs:50`
|
||||
(`GetOrCreate` — `db.Briefs.Add(created); db.SaveChanges();`).
|
||||
- **Extends** — the command/query direction split that `Contracts/Dtos.cs` encodes
|
||||
(`*Request` in / `*Dto` out) and that the banners at `:133`/`:185` state as the file's
|
||||
premise; and concretely, **`POST /brief/reset` (`:712` → `BriefStore.ResetAndCreate`)** —
|
||||
the create-a-fresh-brief command already exists as a POST. The write half of `GetOrCreate`
|
||||
has a command counterpart; the query does not need to duplicate it.
|
||||
- **Baseline citation** — **BL-003**; §7 Backend CQRS-light row.
|
||||
- **The mixing, concretely** — this is the only endpoint in the backend where a GET performs
|
||||
a persisted write. Everything else respects the direction, and notably the read side goes
|
||||
out of its way to _avoid_ writing: `ToDetailDto(DateTimeOffset.UtcNow)` /
|
||||
`ToDto(now)` project Concept → InBehandeling → Goedgekeurd from stored timestamps on every
|
||||
read rather than mutating a status column (`Program.cs:284`, `Data/AanvraagMapper.cs`), which
|
||||
is a textbook CQRS read-model projection and the strongest evidence the convention is
|
||||
intended. `GET /brief` breaks it: a plain read is non-idempotent on first call, allocates a
|
||||
row, and — since the FE retries GETs automatically (`api-client.provider.ts:66`,
|
||||
`retry({ count: 2, delay: 500 })`, GET-only, precisely because GETs are assumed safe) —
|
||||
a transient failure can enter the create path more than once. `BriefStore.GetOrCreate` is
|
||||
`lock`-guarded so no duplicate row results today; the objection is that the safety depends
|
||||
on the lock rather than on the endpoint being a query.
|
||||
- **Proposed change, minimal** — `GET /brief` returns 404 when no brief exists for the owner;
|
||||
`BriefStore.GetOrCreate` splits into `Get` (query) and the existing `ResetAndCreate` (already
|
||||
there). `BriefStore.load()` on the FE (`brief.adapter.ts:55`) treats 404 by calling the
|
||||
existing `reset()` command once. **This is the least certain finding in this file** and the
|
||||
only one with a behaviour change: it costs one extra round-trip on a first visit and touches
|
||||
the brief tests. If the demo-seeding convenience is judged to outweigh the principle, the
|
||||
acceptable alternative is to leave the code alone and add one line at `:603` saying the GET
|
||||
seeds on first call — the defect is as much that it is undocumented as that it exists.
|
||||
- **Effort** — M. Independently shippable, but FE and BE must land together (the 404 contract),
|
||||
so it is the one finding here that is not a single-side deploy.
|
||||
|
||||
---
|
||||
|
||||
## backend — Domain
|
||||
|
||||
**No findings.** `Domain/` is static classes of pure functions (`SubmissionRules`,
|
||||
`IntakePolicy`, `BeoordelingRules`, `HerregistratieRule`, `OrgTemplateRules`, `Authz`,
|
||||
`FeatureFlags`, `LetterHtml`, `DiplomaRules`, `DocumentRules`) with no persistence and no I/O —
|
||||
verified EF-free and ASP-free per §7. A pure decision function has no read/write axis to
|
||||
separate. `Domain/Applications/Aanvraag.cs`'s `Concept`/`Submitted`/`Decided` tagged union is
|
||||
the write model; §3c records 94.2% line coverage.
|
||||
|
||||
---
|
||||
|
||||
## backend — Data
|
||||
|
||||
**No findings within mandate.** The seven static stores (`ApplicationStore`, `DocumentStore`,
|
||||
`BriefStore`, `OrgTemplateStore`, `FeatureFlagStore`, `AuthzAuditStore`, `IdempotencyStore`)
|
||||
each expose reads and writes on one type — `ApplicationStore` alone has 6 reads
|
||||
(`Get`/`List`/`GetAny`/`GetByReferentie`/`ListAll` + `ToDetailDto`) and 7 writes. That is
|
||||
ordinary repository design, and §7 records these as "Not behind any port … Deliberate,
|
||||
documented in `Data/Db.cs`". Splitting them into read/write repositories would be
|
||||
_introducing_ the pattern into a module where §7 records it absent — out of mandate. See the
|
||||
out-of-mandate section. One observation to hand on rather than file: `AanvraagMapper` /
|
||||
`ToDetailDto(now)` is a real read-model projection and is cited approvingly in CQ-007; do not
|
||||
let a future ticket "simplify" it into a stored status column.
|
||||
|
||||
---
|
||||
|
||||
## backend — Zgw
|
||||
|
||||
**No findings.** `OpenZaakZaakSource` / `OpenZaakDocumentSource` implement ports whose
|
||||
interfaces (`IZaakSource`, `IDocumentSource`) already separate by operation
|
||||
(`ListMyCases`/`ListCases` vs `CreateZaak`), and §7 records the ACL as "Fully built" under
|
||||
ADR-0005. §3c gives it the second-highest branch coverage in the backend (85.5%). Nothing to
|
||||
extend.
|
||||
|
||||
---
|
||||
|
||||
## backend — Contracts
|
||||
|
||||
**No findings.** `Contracts/Dtos.cs`'s 65 records split by direction is the backend's
|
||||
strongest CQRS-light artifact and is cited as the pattern several findings above extend.
|
||||
§3c's 65.0% branch coverage (the backend's weakest, per **BL-005**) is agent 02's axis, not
|
||||
this one — a DTO record has no read/write mixing to fix.
|
||||
|
||||
---
|
||||
|
||||
## backend — Stamdata
|
||||
|
||||
**No findings.** Config-as-code tables (ADR-0004), validated at build by
|
||||
`StamdataValidationTests`, never runtime-editable — read-only by definition. Its only
|
||||
endpoints (`Program.cs:164`, `:173`) are both GETs behind the `StamdataAdmin` gate, correctly
|
||||
placed under the reads banner at `:158`. Together with `libs/beheer` (CQ-005) this is the
|
||||
cleanest end-to-end query slice in the repo.
|
||||
|
||||
---
|
||||
|
||||
## Out of mandate (pattern absent)
|
||||
|
||||
Filed here rather than as tickets, per the "name the pattern you extend or don't file it"
|
||||
rule. Agent 08 or a human decides whether any of these becomes a ticket.
|
||||
|
||||
**OOM-A — extracting `Program.cs` into `Features/` folders with handler types.**
|
||||
This is the change BL-003 most obviously invites: 940 lines, 48 endpoints, file CC 78 against
|
||||
a next-highest of 27. It is out of mandate because §7 is explicit that the backend has
|
||||
**"No handler types, no mediator, no `Features/` folders"**, and the local helpers (`Submit`,
|
||||
`StamdataAdmin`, `CasesAdmin`, `Beoordelen`, `OrgAdmin`, `FlagsAdmin`) are authorization and
|
||||
idempotency wrappers, not handlers — verified by reading them at `Program.cs:772-940`. There
|
||||
is no CQRS-light structure here to extend, only one to introduce. CQ-006 is the largest step
|
||||
available _within_ the mandate, and it deliberately stops at ordering and comments. Note also
|
||||
that CQ-006 is a strict prerequisite for OOM-A should it ever be taken: you cannot cut a
|
||||
940-line file into vertical slices while five of its seven sections interleave directions.
|
||||
|
||||
**OOM-B — read/write repository split in `backend/Data`.**
|
||||
`ApplicationStore` (file CC 27, the highest in `Data`) and the six sibling stores each mix
|
||||
reads and writes. Splitting them into query and command repositories would introduce the
|
||||
pattern where §7 records it absent. It would also collide with the documented static/no-DI/
|
||||
`Db.Create()`-per-call design (§7: "Deliberate, documented in `Data/Db.cs` and
|
||||
`Program.cs:40-45`"), which agent 06 may have views on.
|
||||
|
||||
**OOM-C — no read model, no event sourcing, and none proposed.**
|
||||
Stated explicitly so a later phase does not read this file as a step toward one. Baseline §7
|
||||
records no separate read model or event store, so per the role definition neither is in scope.
|
||||
The `ToDetailDto(now)` status projection is a read-side _derivation_, not a materialised read
|
||||
model, and CQ-007 argues it should stay that way.
|
||||
|
||||
**OOM-D — BL-011 affects every acceptance criterion here.**
|
||||
Not a finding, a scheduling note: the FE suite is flaky under parallel load, so "CI green"
|
||||
alone does not verify CQ-001..005. Verify against §3a/§4a numbers, per **BL-009** (no
|
||||
coverage threshold is enforced anywhere, so nothing ratchets).
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| ID | Module | Title | Extends | Baseline | Effort | One deploy? |
|
||||
| ------ | ----------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------- | -------------- | ------ | ----------------------- |
|
||||
| CQ-001 | ssp/registratie | `createDraftSync` is a command factory owning 3 query paths | the command-factory idiom (`submit-change-request.ts`) | §7, §4a, §9 | M | yes |
|
||||
| CQ-002 | ssp/registratie | 2 read stores write without the `runSubmit` fold; errors dropped | `runSubmit` + `createSubmitChangeRequest` | BL-007, §7 | S | yes |
|
||||
| CQ-003 | ssp/brief | `runSubmit` (write fold + idempotency mint) used for 3 reads | `submit.ts` fold vs `withIdempotencyKey` | BL-007, §7 | S | yes |
|
||||
| CQ-004 | libs/shared/application | `FeatureFlagStore.set` skips the fold, drops the error entirely | `runSubmit`/`SUBMIT_FAILED` (same folder) | BL-007, §7 | S | yes |
|
||||
| CQ-005 | libs/beheer | 2 stamdata reads run through `runSubmit`; ship with CQ-003 | `submit.ts` fold vs `withIdempotencyKey` | BL-007, §7 | S | yes (with CQ-003) |
|
||||
| CQ-006 | backend/Program.cs | read/write banner split abandoned in 5 of 7 feature sections | the `:441`/`:464` WP-65 banner pair, in-file | BL-003, §7,§3c | S | yes — land it alone |
|
||||
| CQ-007 | backend/Program.cs | `GET /brief` creates; `POST /brief/reset` already exists | direction split in `Contracts/Dtos.cs`; `POST /brief/reset` | BL-003, §7 | M | **no** — FE+BE together |
|
||||
|
||||
**Modules with no findings:** ssp/auth · ssp/herregistratie · ssp/showcase+shell+root ·
|
||||
bhp/auth · bhp/behandeling (the reference implementation) · bhp/shell+root ·
|
||||
libs/shared/{infrastructure, upload, domain, contracts, kernel, ui, layout, testing,
|
||||
environments} · backend/{Domain, Data, Zgw, Contracts, Stamdata}.
|
||||
|
||||
**Suggested sequencing.** CQ-003 + CQ-005 are one ticket (one shared-file split, five call
|
||||
sites) and should go first — they make the direction legible at every call site, which is
|
||||
what CQ-002 and CQ-004 then apply consistently. CQ-006 is independent and can run in
|
||||
parallel on the backend. CQ-001 is the only FE ticket with real design content. CQ-007 is
|
||||
the only one needing a coordinated deploy and the only one whose premise is arguable —
|
||||
schedule it last, or take its documentation-only alternative.
|
||||
|
||||
**Honest scale.** Six of seven findings are S/M and none is a correctness defect except
|
||||
CQ-004's dropped error and CQ-002's silent rollback. Baseline §6 (0 dependency violations,
|
||||
textbook instability gradient) and §3c (97.6% backend line coverage) are accurate: this is a
|
||||
well-structured codebase, and the CQRS-light work available is consistency work, not repair.
|
||||
Reference in New Issue
Block a user