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>
36 KiB
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.tsas one of the repo's 3 command factories, alongsidesubmit-change-request.ts(18 lines, one command, zero reads) andsubmit-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
createDraftSync143 lines (the largest function in the codebase, tied toreduceUpload's 109 only in the two-memberfn>75population); §9 threshold "TS function > 40 lines". - The mixing, concretely — the factory returns four members.
resume()is pure query orchestration: read?aanvraag,adapter.detail(linked), elsefindConcept()→adapter.list()→parseApplications.submit()/reset()/the debounceeffectare writes. They are entangled through three pieces of shared mutable closure state —id,ensuring, andresumeGate(:56-61) — whereresumeGateexists 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)andloadConcept(adapter, id)as free functions taking the adapter (noinject, so they get a direct spec —draft-sync.spec.tsalready exists and would shrink).createDraftSynckeepsresumeGateand 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) andapps/ssp/src/app/registratie/application/admin-cases.store.ts:46-56(delete); the adapter methods areinfrastructure/applications.adapter.ts:60 canceland: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 thecreateSubmitChangeRequestcommand 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
runSubmitinside the adapter — these reach the rawApiClientand never produce aResultat all. - The mixing, concretely — both stores own a
RemoteDataread signal and a write, and the write's failure path iscatch { this.state.set(before); }— a bare rollback with no error channel. A failed cancel makes the row silently reappear with no message, noActionState, no ProblemDetailsdetail.BriefStore/OrgTemplateStorein the sibling context both hold anActionState+lastErrorfor exactly this. The bareadapter.cancel()also means theIdempotency-Keyon the wire is a fresh UUID minted per HTTP attempt byapi-client.provider.ts:58, not the per-logical-submit keyrunSubmitpromises atsubmit.ts:11-13— that invariant's docstring is currently false for these two calls (harmless today:Program.csonly honours the header inside theSubmithelper, see CQ-005's note). - Proposed change, minimal — route both through
runSubmitand 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 oneerrorsignal; (b) fuller — aapplication/cancel-application.tscommand factory mirroringsubmit-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→Resultfold (which reads legitimately want) and thewithIdempotencyKeywrapper (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-13statesrunSubmitis "the one place a logical submit's Idempotency-Key is minted — once perrunSubmitcall". Each of these reads therefore mints a UUID and assigns the module-levelpendingIdempotencyKey(api-client.provider.ts:21-26) for the duration of a GET. It is inert today: the header is attached only whenmethod !== 'GET'(api-client.provider.ts:58). Butapi-client.provider.ts:15-19explicitly 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()andOrgTemplateStore.load()are awaited acrossawaits — so routing reads through the write helper quietly widens the assumption that comment relies on. Readingbrief.adapter.ts:55-92it is also simply impossible to see which of the seven methods are commands: all seven areawait runSubmit(...) → parseBriefView. - Proposed change, minimal — split
submit.tsin place, no new concept:runResult(fn, fallback)= the existing try/catch +problemDetailfold;runSubmit(fn, fallback)=runResult(() => withIdempotencyKey(crypto.randomUUID(), fn), fallback). Point the 5 reads atrunResult. Zero behaviour change, and afterwards the keyword at each call site states the side.submit.spec.tsalready 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 atlibs/shared/src/infrastructure/feature-flags.adapter.ts:18; caller atlibs/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.tsandstamdata.adapter.ts). - The mixing, concretely — the store owns the read (
load,flags,all,enabled) and the admin write. The write istry { await this.adapter.set(...) } finally { await this.load() }— nocatch. The rejection propagates out ofset(); the caller isvoid 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 thefinally's reload with no explanation and no ProblemDetailsdetail, on a write gated byflags:managethat is exactly the kind an operator needs confirmation of. Every other write in the repo that goes throughrunSubmitgetsproblemDetail(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 oneerrorsignal rendered byfeature-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.tssplit 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.tsexplicitly 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 callrunSubmit. 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:StamdataStorehas no write command at all (download()at:137is a localBlob+ anchor click, zero network), andAuditStoreis read-only. The tooling and the baseline both mis-classify this module purely because of the helper name. - Proposed change, minimal — point both at
runResultper 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::197Document upload,:275Applications,:598Brief,:721Organization 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/:464treatment to the five sections that predate it. No handler types, no mediator, noFeatures/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.cs97.4% line / 84.8% branch. - The mixing, concretely — the file opens by declaring direction as its organising
principle (
:133reads,:185writes), then from:197switches 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/statusPOST /uploads,DELETE /uploads/{id},DELETE /admin/uploads/{id}Applications 275 GET /applications,GET /applications/{id}POST,PUT /{id},DELETE /{id},POST /{id}/submitAdmin cases 424, 554, 566 GET /admin/cases,GET /admin/auditDELETE /admin/cases/{id}— 129 lines away from its list, with werkvoorraad, beoordeling, besluit and the ZGW notification hook in betweenBrief 598 GET /brief,GET /brief/previewPUT /brief,POST submit/approve/reject/send/reveal-bignummer/resetOrg templates 721 GET /admin/org-templates,GET /admin/org-template/{id}PUT /{id},POST /{id}/publish,POST /{id}/rollbackGET /admin/org-template/{subOrgId}/preview(:703) is additionally filed under the Briefbanner 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; moveDELETE /admin/cases/{id}(:554) andGET /admin/audit(:566) up besideGET /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.csencodes (*Requestin /*Dtoout) and that the banners at:133/:185state 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 ofGetOrCreatehas 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 /briefbreaks 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.GetOrCreateislock-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 /briefreturns 404 when no brief exists for the owner;BriefStore.GetOrCreatesplits intoGet(query) and the existingResetAndCreate(already there).BriefStore.load()on the FE (brief.adapter.ts:55) treats 404 by calling the existingreset()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:603saying 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.