Files
ehoandClaude Opus 5 664a43bf2d docs: refactoring-backlog workspace — baseline + 3 Phase 1 agents
Runs the multi-agent refactoring-backlog pipeline in docs/project/
refactor-backlog-setup/ up to and including three of the seven Phase 1
agents.

00-baseline.md establishes the metrics every later agent must cite, using
only tooling already in the repo (vitest lcov, coverlet cobertura, ESLint's
core `complexity` rule at threshold 0 for a full distribution, depcruise
--metrics). Duplication and C# complexity had no tooling, so
tools/baseline-scan.mjs adds a deterministic ~200-line text scan rather
than a new dependency; the approximations are labelled as such.

Headline: FE 75.1% line coverage but only over the 98 of 220 source files a
spec loads; BE 97.6% line / 79.6% branch; 0 layering violations; 7.1%
duplication; 25 of 2085 TS functions over CC 10.

Then 02-testability, 04-cqrs-light and 06-adr-conformance (27 findings).
01/03/05 were skipped deliberately — the baseline shows little for them to
find; 07 (BIO2) and 08 (consolidation) are still open.

Each agent corrected a baseline observation of mine, and in every case the
error was in something derived rather than measured:

- BL-007 counted ~13 adapter "mutations" from the `runSubmit` helper name;
  5 of those call sites are reads. It also missed 3 real mutations that
  reach the raw ApiClient and never return a Result.
- BL-002 diagnosed the 100%-duplicated auth folders as ADR-0002's
  divergence prediction failing. It never had a chance to fail: §3's
  `Principal` union was never built.
- BL-004 named libs/shared/domain and libs/beheer/contracts as coverage
  gaps; both are pure type declarations where 0% is unimprovable.

All three corrections are recorded inline in 00-baseline.md §10, so agent
08 does not inherit the bad numbers.

.prettierignore excludes the agent prompt directories — reflowing their
markdown would edit the prompt text itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:44:32 +02:00

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:lineapps/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:lineapps/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.
  • ExtendsrunSubmit + 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 citationBL-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:lineapps/ssp/src/app/brief/infrastructure/brief.adapter.ts:56 (loadbriefGET), apps/ssp/src/app/brief/infrastructure/org-template.adapter.ts:39 (listorgTemplates) and :51 (loadorgTemplateGET). 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 citationBL-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, concretelysubmit.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 awaits — 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:linelibs/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.
  • ExtendsrunSubmit/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 citationBL-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, minimalconst 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:linelibs/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 citationBL-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:linebackend/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 citationBL-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:linebackend/src/BigRegister.Api/Program.cs:603 (api.MapGet("/brief", …)) → backend/src/BigRegister.Api/Data/BriefStore.cs:50 (GetOrCreatedb.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 (:712BriefStore.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 citationBL-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, minimalGET /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.