## 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` → 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.