RD-03 moved the dashboard page to overzicht/ui/overzicht.page.ts and left four
sections in registratie/ui/dashboard/. The folder was named after a page that
lives in another context. A reader who opened it found four sections that are
not the dashboard.
The folder is now overzicht-secties/ — registratie's sections for the overzicht
page. The alias does not change, because the sections stay in the registratie
context. The story titles do not change, because they name the context.
Five documents cited registratie/ui/dashboard.page.ts, a file that RD-03
renamed. They now name overzicht.page.ts, or the section that owns the
behaviour they describe.
The /dashboard route keeps its path. It is a user-visible URL.
npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md did not name the overzicht context that RD-03 created, and it did
not name the max-lines budget that RD-02 enforces. An agent that follows it
writes a long page into the wrong context, and meets a red build with no
warning.
CLAUDE.md now names the context, the @overzicht/* alias, the
overzicht -> registratie arrow, the 250-line budget and its glob, and the
step-component contract. layers.mdx gains the same arrow.
atomic-design.mdx credited eslint.config.mjs with the layer rules.
dependency-cruiser enforces them. Each tool is now named for what it does.
The page also gains the layer-folder table and the step contract.
Four paths were pre-monorepo: three example paths in the ui-component skill,
and two citations of libs/shared/src/ui/async, which RD-27 moved to
libs/shared/src/ui/molecules/async.
npm run ci --full passes: 67 and 45 storybook suites, 306 axe tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The learning guide fp-tea-atomic-design.md still cited the pre-monorepo
tree. Every path started with src/app/, which no directory has used
since WP-67 split the app into apps/ssp and libs/shared.
Rewrite the 11 stale paths per the ticket's mapping rules: a context
path becomes apps/ssp/src/app/<context>/..., a shared path becomes
libs/shared/src/.... Fix the broken anchor at Part 5b to match the
current ARCHITECTURE.md §1 heading. Fix the "Where" guidance in
Recipe A to name the atomic layer folder under libs/shared/src/ui/.
The teaching prose is unchanged; only addresses moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RD-31 applied the symbols-not-lines rule to section 6a only. Section 2g has
the same rot. Four of its five citation groups point at the wrong code:
herregistratie.machine.ts L138-142 lands on `resolve`, not `setField`;
draft-sync.ts L34 is a blank line; the wizard's L78 is an input attribute;
brief.store.ts L157 is a comment.
One link was also dead. text-input.component.ts moved to
libs/shared/src/ui/atoms/ in RD-27.
Every link in the document now resolves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 6a cited line numbers and two paths from before the monorepo
split (WP-67). A line number goes stale on the next edit; a symbol
survives it. Replace every L<n> citation in the section with the
named symbol, fix the two dead paths (environment.ts now lives under
libs/shared, proxy.conf.json is now one file per app), and point the
read walkthrough at mijn-registratie.section.ts, where the RD-03
split moved the dashboard's <app-async> block.
Mark RD-31 done and update its README row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
app-choice-link rendered a component host between the keuzelijst <ul>
and its <li>. This broke the axe list/listitem rule for assistive
technology. Five story suppressions named WP-11 as the fix, but WP-11
closed with no open ticket left to own the defect.
choice-link now uses selector: 'li[app-choice-link]', the same
attribute-host pattern as application-link. The host carries the
keuzelijst__list-item class; the template drops its own <li>.
Position: relative stays on .keuzelijst__link so the stretched-link
overlay still resolves against the card, not the host.
aanvraag-block needed no component change: it renders a CIBG melding,
never an <li>. Only its story wrapped it in a <ul>, which is what axe
rejected. The wrapper is removed, and the four non-Concept stories are
deleted — the component's template only renders for status Concept, so
they rendered nothing.
All five a11y: { disable: true } suppressions are gone, with no
replacement. atomic-design.mdx now records that both molecules are the
<li>, kept separate for the vendored CSS they bind, not for list
semantics.
npm run ci --full passes, axe included.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The API container used server garbage collection. Server GC makes one heap per
CPU, which is 22 heaps on this host. The two frontend containers sized the Node
heap from host RAM (16 GB), because compose sets no cgroup limit.
Set DOTNET_gcServer=0 and NODE_OPTIONS=--max-old-space-size=2048. This bounds
the memory that `docker compose up` needs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three wizard containers read `this.seed()` in the constructor. Angular
binds component inputs after the constructor runs, so the value was always the
`initial` default, `seeded !== initial` was always false, and every mount took
the `draftSync.resume()` branch. The `seed` input was dead code.
The two single-step forms built on the same idiom read the input inside the
microtask and work correctly. That contrast is the diagnosis.
Impact: 21 seeded wizard stories rendered step 1 instead of the state they
asked for. Storybook is this repo's UI test surface, so the states with no
other coverage were exactly the ones not rendering — Submitting, Submitted,
Failed, Ingediend, Mislukt. The a11y runner checks that whatever rendered is
accessible, never that the right thing rendered, so nothing caught it.
Production was unaffected: no route binds `seed`.
Read the input inside the microtask, matching the two forms. Turn the spec's
old `componentInstance.dispatch(...)` workaround into a real regression test
through `componentRef.setInput('seed', ...)`.
Verified: with the intake fix reverted the two spec cases fail; with it, 319
pass. `npm run ci --full` is green, and the newly rendered markup produced no
axe violations. A browser check of seven seeded stories across all three
wizards asserts text only reachable from a seed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The readability arc: a max-lines guard with self-cleaning exemptions, the
createStore effect map, RemoteData/lifecycle consolidation, the ticket-reference
sweep across apps, libs and backend, six oversized files split by concern, the
libs/shared/ui move into atoms/molecules/organisms, and the atomic ladder
enforced in dependency-cruiser.
RD-16 was dropped: the instruction would have discarded server decisions.
RD-30..RD-36 stay open — documentation updates plus two optional refactors.
RD-22 and RD-23 brought the wizard containers under the 250-line budget, so
`max-lines` reports nothing. The files still read badly. Line count was never
the problem.
Fix three things in all three containers:
1. The member order was scrambled, and it differed per file. `registratie`
declared `draftSync` in the middle of a run of `computed`s; `herregistratie`
read `this.stepLabels.length` seven lines before `stepLabels` existed; the
three files put the copy arrays in three different places. All three now use
one nine-section order, so they compare side by side.
2. Pure logic sat in the container. Extract `digitalDocumentIds` into
`upload.machine.ts` — the "digital and finished uploading" filter was
written out four times, and it removes a `documentId!` assertion from both
containers. Extract `diplomaMsg` into a sibling of the step files.
3. Comments carried archaeology. Drop the three RD-05 references and keep the
rule. Drop "replaces sessionStorage" and the note about focus management that
moved to the shell. Fix `intake`'s class comment, which claimed answers
persist to sessionStorage and was contradicted 30 lines below.
`phase` deliberately stays in all three: it cannot live in `domain/`, and three
siblings plus three specs is a worse trade than 17 readable lines. The store ⇄
`draftSync` cycle also stays — both callbacks are deferred, so it is safe, and
one comment now names it.
No behaviour change. Member lists and every `private`/`protected`/`readonly`
modifier are unchanged, which the showcase depends on.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found while measuring RD-30, which archives the directory these references
point into.
Five stories disable the axe check with the reason "WP-11 reworks this
markup". WP-11 is done, and so is WP-13, the gap register it handed the
remainder to. No open ticket owns the defect, so the rule that a disabled
check must name the ticket that removes it holds only in letter.
The defect ships: `app-choice-link` and `app-aanvraag-block` put a component
host between the keuzelijst `<ul>` and its `<li>`, which breaks axe's
list/listitem rule. WP-11 solved the same problem for `application-link` by
making the host be the `<li>`, but `atomic-design.mdx` documents the current
split as deliberate — so whether that move fights the vendored CSS is the
question RD-37 must answer first.
Sequenced before RD-30.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add three dependency-cruiser rules that forbid upward edges inside
libs/shared/src/ui: atoms-compose-nothing-above, molecules-below-organisms,
and design-system-not-layout. RD-27 made the atomic ladder expressible by
moving components into atoms/molecules/organisms folders; these rules make
it enforced, so an upward import now fails dep:check instead of shipping.
No pathNot exemption for specs and stories: measured, zero upward edges
exist anywhere in libs/shared/src/ui today, in production code, specs, and
stories alike, so the exemption PLAN proposed has nothing to protect.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two components disagreed with their own story title. async.component.ts
had no layer tag; add `Molecule:` to its class header, not to the
Convenience: comment on the ASYNC array (that comment describes an
export, not a layer, and stays as it is). breadcrumb.component.ts had
`Chrome:`; rename it to `Molecule:` to match its story title.
CLAUDE.md and layers.mdx both claimed `libs/beheer/ui` is Design System.
The code disagrees: its story title is `Domein/Beheer/...`, because
libs/beheer is a bounded context that lives under libs/ only because two
apps share it. Fix the two doc lines to match the code; the story title
does not change.
The ticket's own tag-count check asserted 68 after the edits, but adding
a new tag to async.component.ts (which had none) is a net +1 over the
Chrome-to-Molecule rename (a wash) — the true post-edit count is 69.
Corrected the number in the ticket text rather than deleting the
mandated tag to force the wrong count. Also fixed pre-existing invalid
nested-backtick markdown in the ticket's decision 4 bullets: prettier
mis-rewrapped it and dropped spaces, so the phrasing was rewritten in
valid CommonMark with no change in meaning.
npm run ci --full passes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Decision 3 said the `@shared/ui/` occurrence count must not change at 200,
while decision 4, nine lines below, mandated converting five relative imports
into `@shared/ui/` aliases — five new occurrences by construction. The answer
is 205, and the agent verified by hand that the 21 occurrences now inside
`libs/shared/src/ui/` are the 16 from before plus exactly those five.
This is the fourth instance of the pattern named in the previous docs commit,
and the first ticket written after naming it. Recorded as such: when two
decisions both touch the thing you are counting, add them up first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The folder now equals the layer, as CLAUDE.md decision 2 requires. 33
directories move by git mv (25 flat, plus upload/'s 8 subfolders split
across all three layers). 28 distinct @shared/ui/* specifiers rewrite
across 73 files, longest-first. Five relative imports inside upload/
become @shared/ui aliases because their sibling now lives in a
different layer; two stay relative because both ends stay in the same
layer. Four .mdx docs get their seven broken story imports fixed;
atomic-design.mdx's page-shell import is untouched, because layout/
does not move.
No component, template, story title, or layer-tag comment changes.
That is RD-28's job.
Verified against the ticket's acceptance commands: the 26 flat
directories become exactly 3 layer folders with the counts the ticket
names, only three @shared/ui/* prefixes remain (atoms, molecules,
organisms), the .mdx import count holds at 7, and the relative-import
count inside ui/ drops from 7 to 2 as decision 4 requires. The
@shared/ui/ occurrence count moves from 200 to 205: decision 4
mandates turning 5 of those 7 relative imports into @shared/ui/*
aliases, which decision 3's "200 before, 200 after" check does not
account for. The 5-occurrence gap is exactly the 5 conversions decision
4 names, not a lost or duplicated specifier.
npm run ci --full passes: lint, typecheck, dep:check, format, tokens,
seam, both apps' + both libraries' tests, both apps' localized build,
audit, backend tests, all three generated-artifact drift checks, and
both Storybook instances' build + axe-core a11y suite (67+45 suites,
198+112 tests, all green).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Twenty of letter-canvas's twenty-eight input()s are $localize labels that no
caller ever binds. Inline nineteen of them as template i18n, using the same
ids and source text so messages.en.xlf does not change. recipientText stays
an input() because its message embeds a literal \n, which as template text
becomes a different source string to Angular's extractor.
Extract letter-line.component.ts for the #line template plus the
label/auto/state/sample helpers it needs, replacing letter-canvas's three
ngTemplateOutlet incantations with one tag each. Its helpers are exported
pure functions with a spec, no TestBed.
The file stays over the 250-line budget (77 lines of CSS plus one letter's
markup), so the eslint-disable max-lines directive stays too, with its
reason rewritten to say so plainly — the only such disable left in the repo.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Decision 4 said the parent keeps seven outputs while decision 5, four lines
below, said all eleven remain and are re-emitted from children. Seven is how
many the parent raises; eleven is how many it declares. The acceptance line
copied the wrong one, and satisfying it would have broken
`org-template.page.ts`, which binds all eleven.
RD-08, RD-23 and RD-25 are the same mistake: a decision describes the design
in one vocabulary, and the acceptance line counts something else that shares a
word. Name what the command counts before writing the number.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
org-template-editor.component.ts carried an eslint-disable for max-lines,
padded by a dead sample-letter constant, 13 label inputs that were never
bindable, and two self-contained mutation clusters. Split all three out:
- SAMPLE_LETTER_BRIEF moves to brief/domain/sample-letter.ts. It is
production content (the letter the admin previews), not a test fixture,
so it stays out of brief.testing.ts (no-testing-in-production forbids
production code from reaching a *.testing.ts file).
- 11 of the 13 label inputs become inline i18n template text. The two
that interpolate MARGIN_MIN_MM/MARGIN_MAX_MM (marginsLegend,
invalidHint) stay in TS, because moving an interpolated $localize call
into a template renames the xlf placeholder and breaks the translation
merge. Every id is preserved; messages.en.xlf is unchanged.
- logo-upload.component.ts and version-history.component.ts each take
one output cluster. The parent still declares and re-emits all 11
outputs — org-template.page.ts binds them directly on
<app-org-template-editor> and is out of this ticket's file scope, so
the parent's public surface cannot shrink.
Correction to the ticket while executing it: its acceptance check for
"= output" on the parent read "MUST be 7", copying decision 4's cluster
count instead of decision 5's (and the ticket's own Risks section's)
explicit requirement that the parent keep all 11 declarations. Fixed the
ticket's acceptance section to the correct number.
npm run ci --full is green.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A task notification fires every time an agent stops with no live children,
including a pause mid-task. RD-24's agent paused and notified; the supervisor
read that as abandonment, took over the tree, ran the gate and edited the
ticket Status while the agent was still working. The agent resumed, correctly
detected a second writer, and refused to commit.
This is the mirror image of the RD-17 race the section above records: there a
fresh supervisor met a running agent, here a running agent met its own
supervisor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page held six teaching sections and a 142-line `styles:` block, at 471
effective lines against a limit of 250. It is now 36 lines of composition.
Angular scopes a component's CSS to markup that component rendered, so the
split had to move each rule to its owner. `concept-card` owns the card
vocabulary and renders it. `.app-code`, `.app-lead`, `.app-cols` and
`.app-note` become globals, because their targets are projected or arrive
through `[innerHTML]`.
That constraint exposed a live bug. The syntax-highlighting rules compiled to
`pre[_ngcontent-%COMP%] .k[_ngcontent-%COMP%]`, but `highlight-ts` injects the
`.k`/`.s`/`.c` spans through `[innerHTML]`, so they carry no scope attribute
and the rule never matched. Keywords, strings and comments have always
rendered in the plain foreground colour. The rules are global now, on five new
`--app-code-*` tokens.
Widen the colour guard while here: it scanned only `*.component.ts`, so every
`*.page.ts`, `*.section.ts` and `*.step.ts` was invisible to it. That is how
this page collected 21 hardcoded colours. One other file needed a fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both repeat a habit this README already documents, so they belong beside it
rather than as new entries.
`git grep -c` counts lines: a symbol that a file imports and then calls is two
lines, never one. And a check must not grep for a word that the ticket's own
mandated snippet contains — RD-23 required the upload controller's
`dispatch:` property and then asserted zero occurrences of `dispatch`.
The agent hit both, refused to distort the code, and corrected the ticket text.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the adres, beroep and controle cases out of registratie-wizard.component.ts
into adres.step.ts, beroep.step.ts and controle.step.ts, matching RD-22's
*.step.ts convention. The parent drops from ~568 to 274 lines and loses its
`eslint-disable max-lines`.
The upload controller moves into beroep.step.ts and emits `uploadMsg` instead of
dispatching directly; the parent maps that back onto the machine's `Upload`
message. `onDiplomaKeuze` stays in the parent (message construction from the DUO
payload belongs in the container) and now takes only the chosen id, reading its
own `duoData` computed instead of receiving the DUO payload as an argument. Each
step injects `RegistratieLookupStore` directly for its own async presentation
(adresStatus, the DUO lookup, samenvattingVragen) — the sanctioned exception,
since it is a root singleton. Markup moved verbatim; the `@@` id count across
the directory stays 43.
Two of the ticket's acceptance numbers do not hold against correct code and are
corrected in the ticket file: `createUploadController` is 2 lines (import +
call), not 1 — `git grep -c` counts lines, and the same shape gives 2 for
`createStore` and 3 for `createDraftSync` elsewhere in this codebase. `dispatch`
is 1, not 0 — decision 4's mandated `UploadControllerDeps.dispatch` property
name is that string even though it is not the machine's dispatch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The parent held one @switch with three @case blocks — three screens'
markup in one file. Each case is independent and needs only the
answers, the errors, and (for two of them) the scholing threshold.
Extract buitenland.step.ts, werk.step.ts, and review.step.ts as pure,
presentational steps: inputs down, one narrow output up, dispatch
never passed down. The parent keeps the store, the shell, and
draftSync, and maps each step's output back to a machine message.
This is the first *.step.ts in the repo, so it sets the naming
convention that RD-23 does the same job with.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
deleteAdjacentChip and insert did getSelection()/Range work inside the
component, which pushed it over the max-lines budget under a disable
comment. rich-text-dom.ts already owns the DOM boundary, so the surgery
moves there as two new exports, chipAtCaret and insertChipAtCaret, and
the component keeps only its event-handling and output concerns.
adjacentChip stays exported with its own spec case. The component
disable comment is gone, since the file is now under the line budget.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-03 moved the dashboard page into the `overzicht` context and RD-04 left the
folder name alone, recording it as stale. It stays stale: the folder is named
after a page that lives somewhere else, so a reader looking for the dashboard
finds four sections that are not it.
Measured cost: 8 `git mv`s and four import lines in one file. Fold in two doc
paths that still name the moved page.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RD-20 asserted `git grep -l "toWizardErrors"` would find 5 files, but its own
Steps list regenerates `behaviour-spec.mdx`, and the generator publishes every
`describe` title. Naming a spec after the function it tests puts the name in
the generated document too, so the honest count is 6.
The agent refused to rename the describe block to satisfy the number, which is
the correct response and matches the precedent from RD-14.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each wizard flattened its per-field error record into the shell's
WizardError[] summary with its own copy of the same loop. Extract one
pure helper, wizard-errors.ts, next to naarStapLabel. Add a spec that
covers a flat record, an empty record, skipped undefined/empty-string
values, the idPrefix, and a skipped nested object.
registratie-wizard.machine.ts changes Errors from an interface to a
type alias, because only a type alias gets an implicit index
signature and is assignable to the helper's Record<string, unknown>
parameter. The other two machines already declare their error maps as
type aliases, so this also makes the three consistent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-18 established that a ticket reference inside a path to a document that
still exists is a pointer, not provenance, and exempted one. RD-19 did not
re-run that check against `backend/`, and asserted a target of 0. Two such
paths existed, so the agent described the two documents in prose instead. The
path no longer resolves and the reader must search.
Restore both paths, correct the ticket's decision 1 and acceptance target to
2, and record the miss as the eighth in the README's list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backend half of the sweep RD-18 did for the front end. git blame
holds the provenance and stays correct when the code moves; the
comment names a closed ticket and tells the reader nothing the
sentence around it does not.
public/letter.css and LetterHtml.golden.html change together, because
the renderer inlines the CSS and the golden file snapshots the
result.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
204 WP-NN/RB-NN comments named a closed ticket instead of the code they
sit next to. git blame already records history and stays correct when
code moves; the comment does not. This sweep removes the reference and
keeps the sentence, across 95 files in apps/ and libs/ plus the
behaviour-spec generator's header text.
Eleven references stay: five story files justify an a11y disable per
the README's rule, and one line in a11y.mdx documents that convention.
Two sentences needed a rewrite, not a deletion, so the reference's
meaning survives its removal. behaviour-spec.mdx is regenerated, not
hand-edited.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three dashboard-section stories used a fourth title segment
(`Domein/Registratie/Dashboard/<Name>`) that the sidebar rule does not have.
Drop the `Dashboard/` segment so all story titles follow the one rule from
CLAUDE.md decision 5.
Add the two missing stories for sections that have more than one visual
state: `wat-moet-ik-regelen` (4 states) and `wat-wilt-u-doen` (2 states, the
first story in the new `Domein/Overzicht/` bucket). `beheer-links` gets no
story — it has one visual state and its other branch renders nothing.
The `MetTaken` story disables the a11y addon with the same reason and WP-11
reference already used on `task-list.stories.ts` and `choice-list.stories.ts`:
`app-choice-link`'s host sits between the keuzelijst `<ul>` and its `<li>`,
a pre-existing structural gap this ticket does not fix.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`/clear` ends the supervisor's context. It does not stop the developer agent
that session spawned. A fresh supervisor read the half-finished tree as an
interrupted session and dispatched a second agent onto the same ticket. Both
agents then ran `dotnet test` in one checkout, which truncated the shared
SQLite file and produced the stale-database failure the Troubleshooting
section already records under a different cause.
Also record that a bare `dotnet test` runs the OpenZaak integration test that
both CI scripts filter out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eight sites hand-rolled `rd.tag === 'Success' ? rd.value : fallback`. Six
take the new `successOr(rd, fallback)`, one takes the existing `successOf`,
and one (`big-profile.store.ts`) uses the existing `map`, since it returns a
RemoteData rather than an unwrapped value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-16 was to make parseDashboardView return BigProfile directly, on the
plan's claim that DashboardViewDto, DashboardView and BigProfile were
three names for one payload. Reading the type disproves it.
DashboardView is a pair of BigProfile and HerregistratieDecisions.
BigProfile is { registration, person } and has nowhere to put decisions,
so returning it directly would silently drop the server-computed
herregistratie eligibility — the value ADR-0001 says the front end must
render rather than recompute.
The store's two map calls are not a redundant hop either. They project
one aggregate into two independently consumed signals, and six files read
them separately.
Also withdraw the earlier correction that "Step 2 did not fully land".
That claim came from reading the parse signature without reading the type
it returns. Commit 42e7a1e did the right parts, including moving
HerregistratieDecisions into domain, and correctly left alone the part
that would have been wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository carried 22 abandoned agent worktrees under
.claude/worktrees/, left behind by past agent runs. They are
gitignored, so they never reached a commit, but they stayed on disk
and every unqualified repository-wide grep or find walked all 22
copies of the source tree.
Measured before: 48,005 files under .claude/worktrees/, against 856
tracked in the repository. An unqualified search walked 56 times more
files than the repository contains.
The verification gate confirmed both safety conditions before removal:
all 22 worktree-agent-* branch tips were already ancestors of main, and
all 22 worktrees were clean (unmerged: 0, dirty: 0).
Removal steps:
- git worktree remove for each of the 22 worktrees (no rm -rf, so the
registrations in .git/worktrees/ stay consistent)
- git branch -d for each worktree-agent-* branch (lowercase -d, so an
unmerged branch would block deletion instead of being force-deleted)
- git worktree prune to clear administrative entries
Measured after: 0 files under .claude/worktrees/, .claude/ shrank from
4.7 GB to 72 KB. The 856 tracked files are unchanged. HEAD is unchanged
from before the removals. npm run ci exits 0.
This ticket changes no tracked source file. The diff is this ticket
file and the README row, because the work is entirely in gitignored
paths and local branch refs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-14 asserted that a grep for the four SaveState tags would count at
least 4. All four live on one line of a single-line type declaration, so
the honest answer is 1. The executing agent refused to reformat the type
across four lines to satisfy the number, which would have fought prettier
and violated the ticket's own "move verbatim" decision, and flagged the
check instead.
Use `grep -o | wc -l` when you mean occurrences. Say "lines" when you
mean lines.
This is the seventh miscalibrated acceptance command in this backlog, and
the third an executing agent caught using these rules rather than the
supervisor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RD-12 and RD-13 moved both ActionState consumers into their machines. This
left ActionState with zero users, so this ticket deletes it outright.
SaveState is different. It has two four-way consumers (brief.page.ts and
org-template.page.ts) that still switch on all four cases, so it survives.
It moves into debounced-save.ts, next to createDebouncedSave, the only
function that sets it. Both store imports merge into the existing
debounced-save import line. The doc comments that named ActionState are
reworded, since the type no longer exists. No store's public saveState
signature changes, so brief/ui/ needs no edit.
This completes the phase. Two encodings survive: RemoteData for fetched
data, and each machine's own state union. SaveState stays as an explicitly
justified third encoding, for a separate concern (debounced autosave status)
that the other two do not cover.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Before this change, org-template.store.ts held the action lifecycle in an
actionState signal and the publish impact-confirm gate in an independent
pendingPublish signal. The two were representable in combination, so
pendingPublish === true and busy === true could both hold at once. That
state was meaningless: the UI would show the publish-impact confirmation
while a publish was already in flight.
OrgTemplateState.Loaded now carries one action field, a four-variant union
(Idle | ConfirmingPublish | Busy | Failed). ActionStarted overwrites the
field straight to Busy from any prior tag, so ConfirmingPublish and Busy
can never coexist — not by convention, but because one field can only
hold one tag. requestPublish and cancelPublish become dispatches
(PublishRequested/PublishCancelled); as the reducer already no-ops
outside Loaded, this changes no behaviour. The other four commands
(confirmPublish, rollback, proefbrief, flushSave) keep their existing
loaded() guards. busy, lastError and pendingPublish stay on the store as
computed values reading the new union, with byte-identical public
signatures — no file under brief/ui/ changes.
Ran gen:behaviour-spec for the six new reducer cases.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The sixth acceptance-check miss, and the first the executing agent
caught using these rules rather than the supervisor. RD-12 asserted
`git grep "ActionState" -- apps/ssp/src/app/brief` returns nothing, but
org-template.store.ts lives in that directory and is deliberately out of
scope until RD-13. The check could never pass.
Sharpen the existing habit into a mechanical step: name the files, never
a parent directory.
The agent also caught the RD-11 substring trap in its own work, where the
new type BriefActionState contains ActionState, and word-anchored the
check instead of renaming away from the natural name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The action lifecycle (Idle | Busy | Failed) lived in an imperative
store-level signal, set from ten call sites outside the reducer. The
reducer could not enforce which action transitions are legal.
Add `action` to `BriefState.Loaded`, driven by three new messages
(ActionStarted, ActionFinished, ActionFailed) and handled in `reduce`.
Replace every `actionState.set(...)` call in `brief.store.ts` with the
matching `dispatch`. `BriefLoaded` resets `action` to Idle, so a fresh
load clears a stale action error instead of letting it outlive the
reload.
`busy` and `lastError` stay as `computed`s on the store with a
byte-identical public signature — they are the render seam for four
components and two page templates, and the union belongs in the
machine, not the components. `revealBigNummer` still sets only
`Failed`, never `Busy` — an existing asymmetry, not changed here.
`SaveState`, `org-template.store.ts`, and `pendingPublish` are out of
scope (RD-13, RD-14).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-11 asserted that `git grep "machineRemoteData\|LoadLifecycle"` returns
nothing, but the replacement it mandates is named `fromLoadLifecycle`,
which contains the old name as a substring. The check could never pass.
When a new name contains the old one, anchor on word boundaries.
Add a fourth habit: prefer a number over a prohibition for anything that
must not change. "Do not rename BriefStatus" invites reasoning around it.
"This count must still be 54, and if it moves, revert rather than adjust
the number" does not. RD-11 renamed tags across 19 files with a wire
contract in the same file, and the count held.
Also record a real trap found during RD-11: stale gitignored
bigregister.db artifacts fail backend tests with "no such table" on an
otherwise clean tree. A 0-byte file months old failed 6 tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`machine-remote-data.ts` defined a third encoding of an in-flight fetch:
`LoadLifecycle`. It had three call sites, all one identical line, and the type
was never imported by name. Move the mapping into `remote-data.ts` as
`fromLoadLifecycle`, beside its neighbour `fromResource` — a `RemoteData`
constructor, not a sixth encoding.
The lowercase `loading`/`failed`/`loaded` tags on `BriefState`,
`OrgTemplateState` and `StamdataEditorState` existed only because
`LoadLifecycle` required them. Now that the constraint is inline and
PascalCase, the three machines' load-lifecycle tags become `Loading`,
`Failed` and `Loaded` — matching their own PascalCase message tags in the
same file. `stamdata-editor.machine.spec.ts` no longer asserts a PascalCase
message producing a lowercase state.
`BriefStatus` (the letter's draft/submitted/approved/rejected/sent status,
parsed off the wire from `BriefViewDto`) is a separate tag family and is
untouched — its tag count stays 54 before and after this change.
Delete `machine-remote-data.ts` and merge its spec into `remote-data.spec.ts`.
Regenerate `behaviour-spec.mdx` (the `machineRemoteData` section heading
becomes `fromLoadLifecycle`) and confirm `gen:snippets` reports no drift, since
`remote-data.ts` carries a showcase region.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The wizard shell took two inputs to say one thing: a flat WizardStatus
string and a separate errorMessage input. Each wizard needed three
computeds (failedError, errorMessage, shellStatus) to take the state
apart and put it back together for the shell.
WizardPhase replaces both inputs with one discriminated union. Its
Failed variant carries the message directly, so no data travels through
a second channel. Each wizard now maps its own tags onto WizardPhase in
one computed, composing the localized failure prefix at the same spot
errorMessage did before. The three machines and their own vocabulary
(Editing/Answering/Invullen, Indienen/Ingediend/Mislukt) are unchanged;
only the shell's input contract changes.
The shell reads the Failed message via the existing whenTag helper,
because @switch cannot narrow a union in an Angular template.
Both $localize ids (wizard.indienenMislukt, regWizard.indienenMislukt)
keep byte-identical source text, so no locale file changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Four acceptance commands in this backlog could not pass as written. The
fourth, in RD-09, reached this backlog's own ticket files and 22
gitignored worktrees, so satisfying it literally would have rewritten the
history of completed tickets.
The general fix is `git grep` instead of `grep -r`: it searches tracked
files only, so untracked and gitignored paths cannot pollute the result.
Measured here, `grep -r` finds 132 hits under .claude/ where `git grep`
finds none. RD-17, RD-18 and RD-19 are repo-wide sweeps and depend on
this.
Also correct RD-09's Order row. It claimed the ticket covered a generator
and a skill file; neither teaches the deleted idiom, as recorded in
PLAN.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
RD-05 through RD-08 replaced the hand-called submit method with
createStore's effect map. Two teaching documents still showed the old
method in a code block, as the answer to "how does a submit happen?".
Both blocks also called a function that no longer exists.
Rewrite the code block in ARCHITECTURE.md section 2d and its
fp-tea-atomic-design.md counterpart. Both now show the effect map, keyed
on the Submitting tag, using the same herregistratie worked example with
its optimistic begin/confirm/rollback calls. Both use draftSync.submit,
the call the two herregistratie wizards make today.
State the two properties the old idiom lacked, since they are the reason
for the change: entering a state runs its effect, so a dispatch cannot
skip it; and double-submit protection is structural, because the effect
fires only on a tag transition. Add one sentence on the Seed exemption: a
mount or restore message must not trigger a submit.
Fix the one runIfSubmitting() hop in the write walkthrough at
ARCHITECTURE.md's line 574. The rest of section 6a stays stale on
purpose — RD-31 owns it, including its line citations and dead paths.
fp-tea-atomic-design.md's broken pre-monorepo paths stay stale too —
RD-32 owns those.
Set RD-09's Status to done and its README row to done in the same
commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The three wizards paired a dispatch with a hand-written effect call
(onPrimary/onRetry + runIfSubmitting/runIfIndienen). A missed call failed
silently. RD-05 added the effect map and RD-07 added the Primary message;
this ticket moves each wizard onto both.
Each wizard now registers its submit effect on createStore, keyed by its
own submitting tag (Submitting for herregistratie and intake, Indienen for
registratie — the type catches a wrong key at compile time). The optimistic
begin/confirm/rollback calls stay inside the effect body, unchanged. The
template dispatches Primary and Retry directly, matching how Back already
worked. onPrimary, onRetry, and runIfSubmitting/runIfIndienen are deleted
from all three components.
herregistratie-wizard drops under the 250-rule-line budget, so its
eslint-disable max-lines header is removed in this same commit (RD-02's
self-cleaning mechanism). intake-wizard and registratie-wizard stay over
budget and keep theirs, both already flagged for RD-22/RD-23.
Three doc comments (in the three machine files, plus one in store.ts) named
the deleted onPrimary()/runIfSubmitting() identifiers in prose. Reworded
them so the "idiom is gone from the repo" grep check is not defeated by its
own explanatory comments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
PLAN.md stated that plop-templates/form-machine.hbs generates
runIfSubmitting, and that .claude/skills/form-machine/SKILL.md teaches
it, so the next scaffolded form would recreate the bug RD-06 fixed.
Both are false. The plop template is machine-only: 74 lines, no
@Component, no runIfSubmitting. The skill never mentions it.
Two real sites remain, both teaching the deleted idiom in a code block:
ARCHITECTURE.md:314 and :574, and fp-tea-atomic-design.md:342. RD-09 is
therefore a two-document fix, still worth doing, but not the urgent
generator fix this plan claimed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each wizard component re-derives the step-boundary decision the reducer
already owns: advance on a middle step, submit on the last step. This
ticket moves that decision into the machine, so RD-08 can replace the
component's guard with one dispatch.
Add a `Primary` message to each Msg union, and export a `primary(s)`
function next to the existing `next`/`submit` pair. `primary` is a
three-line branch that delegates to `next`/`submit` and writes no new
validation. Each machine tests "last step" in its own vocabulary, per
the ticket's Decisions block: `herregistratie` checks `step === 3`,
`intake` checks `currentStep(s) === 'review'`, `registratie` checks
`currentStep(s) === 'controle'`. `Next` and `Submit` stay in every
union and every reducer — `Primary` is purely additive.
Add 3 spec cases per machine (9 total): Primary advances from a
non-final step, Primary submits from the final step, and Primary is a
no-op outside the editing state. Each case also asserts the
equivalence the ticket requires for RD-08's migration:
`reduce(s, Primary)` equals `reduce(s, Next)` at a non-final step, and
equals `reduce(s, Submit)` at the final step.
Regenerate `behaviour-spec.mdx` for the 9 new `it()` titles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
scripts/ci-local.sh gated its two storybook + axe steps on
`[[ "${1:-}" == "--full" ]]`, so the flag had to arrive as a positional
argument. CLAUDE.md documents `npm run ci --full`, but npm parses that
flag itself and exports `npm_config_full=true` instead of passing it
through. Proven with `npm run env --full`.
The documented command therefore skipped both steps and still printed
"local CI passed" — a gate that reported success without running. Anyone
following CLAUDE.md has been skipping storybook-a11y.
Accept either form. Fixing the script makes every existing document
correct, instead of changing four of them and re-teaching the reader.
Found while verifying RD-06, which adds stories and so depends on those
steps actually running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Before this fix, a failed submit on the besluit-form or the
change-request-form left the user stuck. Both templates rendered `Failed`
through the same `@else` branch as the editable form. In besluit-form the
fields read from `Editing` only, so they went blank. In change-request-form
the fields still showed the sent value, but `SetField` only applies to
`Editing`, so typing did nothing. In both forms the submit button stayed
enabled, but `Submit` is a no-op outside `Editing`. The only escape was a
page reload.
After this fix, `Failed` gets its own template branch: an error message, a
read-only summary of what was sent (an `app-data-block`, reused from the
existing BRP-address pattern), and a "Opnieuw proberen" button that
dispatches `Retry`. Both machines already handle `Retry` (`Failed ->
Submitting` with the preserved data), so no machine change was needed.
Both components also move to `createStore`'s effect map (RD-05): the
`Submitting` handler replaces the hand-called `runIfSubmitting`, so
`onSubmit` is now a single `dispatch`. `runIfSubmitting`/`runIfIndienen`
now remain only in the three wizards, migrated later by RD-08.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
createStore now takes a third, optional StoreEffects map. Each key is a
Model tag. The store runs that tag's handler after update() returns, and
only when the store enters the tag: the previous tag differs from the new
tag, and the message is not Seed (the mount/restore message in every
machine that has one).
This closes the gap where a component had to call dispatch(msg) and then
a private runIfSubmitting() by hand, or state got silently stuck. No call
site changes here. RD-06 and RD-08 migrate the 5 components that
duplicate that pattern today.
The effect map is a conditional type, not a generic constraint, so a
tagless Model (store.spec.ts's plain number store) still resolves it to
never and needs no third argument. Both tag checks use a typeof/in guard
for the same reason.
Regenerated libs/shared/docs/behaviour-spec.mdx for the 5 new spec titles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RD-03 added `overzicht: ['registratie']`, so the comment claiming
`herregistratie: ['registratie']` is "the one sanctioned cross-feature
edge" became false. There are two, and both point at registratie.
State what each edge is for instead of counting them, so the next
context added does not make the comment wrong again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dashboard page lived inside registratie, a context with no permitted
outbound edges. Two of its sections do cross-context navigation: action
links to herregistratie/intake/brief/concepts, and an admin links section
that imported ADMIN_LINKS directly from the app shell.
Move the page and these two navigation sections into a new overzicht
context. Declare overzicht: ['registratie'] as the one sanctioned edge, so
the four data sections that stay in registratie/ui/dashboard/ remain
reachable. The route stays at /dashboard: it is a user-visible URL and four
e2e specs assert it, so only the import path and class name change.
beheer-links.section.ts now injects HEADER_ADMIN_LINKS, the same token the
site header uses, instead of importing the app shell's nav config directly.
Used npm run gen:context and corrected its three known-wrong outputs: the
overzicht: [] edge, the scaffolded overzicht.page.ts (the real page is the
moved dashboard.page.ts), and the scaffolded path: 'overzicht' route.
Verified the boundary is enforced, not decorative: a temporary import of a
herregistratie file into overzicht.page.ts makes dep:check fail, as
expected, then reverted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The dashboard refactor proved a page can be 42 lines. This rule holds
the rest of the app to that budget, before the split work in RD-20
through RD-26 begins.
Add max-lines at 250 (skipBlankLines, skipComments), scoped to
{apps,libs}/**/*.{page,component,section,step}.ts. The glob includes
section and step, the file kinds the dashboard refactor invented, so
new files from this arc do not escape the guard.
Add linterOptions.reportUnusedDisableDirectives: 'error' repo-wide.
ESLint 9 only warns on an unused disable by default, so a stale
exemption would not fail the build. At 'error', every later file
split must delete its own exemption or the build goes red.
Add a dated /* eslint-disable max-lines */ header to each of the
seven files that exceed the budget today, each with a reason and the
ticket that removes it. letter-canvas keeps its disable after RD-26,
because most of its lines are CSS and the rest is one letter.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Convert the approved readable-codebase plan into the ticket protocol this
repo already uses for long arcs (docs/project/backlog/README.md, 75 work
packages driven to done through it).
Adds docs/project/readable-codebase/ with:
- README.md — the ledger: session protocol, GREEN definition, restart
recovery, the agent loop, and an Order table of 35 tickets with their
dependencies and status.
- PLAN.md — the design record, committed so the repository holds the
complete state. A fresh session needs nothing outside it.
Two properties make the arc restart-safe. Each ticket updates its own
Status line and its README row in the same commit as its code, so the
ledger and git log cannot desync. And the plan lives in the repository,
so recovery is three commands and needs no prior context.
Records one correction found while scaffolding: the 22 abandoned agent
checkouts under .claude/worktrees/ are live registered git worktrees, not
orphaned directories. RD-15 must use `git worktree remove` and verify each
branch tip is an ancestor of main first. Spot-checked four; all are
already merged via 637d500.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Catches drift accumulated since the Application → Aanvraag rename
(Step 1/8, Step 2/8) plus the new tests from this branch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
BrpAddressDto's fields are generated as optional, so returning it
directly from parseBrpAddress lost the narrowing the runtime check
already did. This broke the build once registratie-lookup.store.ts
assigned the parsed address into a stricter local type. Map to a
proper BrpAddress domain shape at the trust boundary instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Each dashboard section (Mijn aanvragen, Wat moet ik regelen, Mijn
registratie, Specialismen, Wat wilt u doen, Beheer) now owns its own
store access, async state, and template. DashboardPage becomes pure
composition.
Extract the repeated RemoteData Success-narrowing pattern into
successOf() and the dashboard sort/split logic into
sortForDashboard/concepten/ingediend, both with tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dashboard-view.dto.ts and brp-address.dto.ts each shadowed a generated
type: DashboardViewDto was declared twice (hand-written with required
fields, generated with everything optional), reconciled only by
structural typing. Both are gone.
dashboard-view.adapter.ts now imports the generated DashboardViewDto/
RegistrationDto/PersonDto/RegistrationStatusDto directly. Its parse
does real work now instead of an identity copy: parseRegistrationStatus
validates each status variant's required fields per-tag (the generated
type flattens the union, so a Geregistreerd row missing
herregistratieDatum previously passed the boundary unnoticed — it no
longer does). HerregistratieDecisions moves from contracts/ to
domain/registration.ts, so no contracts-typed value reaches a page.
brp.adapter.ts drops its own BrpAddressDto shadow the same way.
Hand-written contracts/*.dto.ts count: 4 -> 2 (duo-diplomas.dto.ts and
stamdata.dto.ts remain — both parse fields codegen can't type at all).
Part of the dashboard-readability refactor (see the approved plan).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The wire said Application, the domain said Aanvraag — one aggregate with
two names at every hop. Rename the backend DTOs and the /applications
route to /aanvragen, regenerate the typed client, and rename the frontend
adapter/store to match.
Renamed: ApplicationSummaryDto/DetailDto, CreateApplicationRequest,
SubmitApplicationRequest/Response → Aanvraag* equivalents;
ApplicationsAdapter/Store → AanvragenAdapter/Store;
applications.adapter.ts/applications.store.ts → aanvragen.*.
Left untouched: the admin Case/Zaak vocabulary (/admin/cases,
AdminCasesStore) — a separate read model, not part of this rename; the
internal BigRegister.Domain.Applications namespace and the Applications
EF table (renaming those needs a new EF migration, out of scope here).
Part of the dashboard-readability refactor (see the approved plan).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docker compose up at the repo root does not start OpenZaak. A new reader
could easily assume it does. Add a short section to
backend/openzaak/README.md that shows how to check the four containers and
how to curl OpenZaak directly, using the same probe
bootstrap-catalogus.sh already relies on. Add one line to the root README
pointing there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Closes the CD refactor backlog (docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md).
All 33 code tickets and the four gated ADR-fixes (ADR-C-001, ADR-C-003,
ADR-C-007, ADR-C-009) are merged, one commit per ticket, across six CD
batches plus the ADR-fix batch. npm run ci is green after every merge in
the arc, each verified independently.
Highlights: RB-01/02 fixed a BSN leak in the persisted audit trail and an
unauthorized document-content endpoint. RB-09/13 landed Session -> Principal
per ADR-0002. RB-12 added a route-table authz gate as a CI safety net.
RB-19 reordered the backend's 940-line Program.cs into reads-then-writes,
verified as a pure move by comparing every (route, gate, handler) triple
before and after. RB-24..30 moved libs/shared/upload into its proper
layers and made every layer testable. RB-31 found and fixed a real
ADR-0006 violation: two tests asserted a wizard state the real reducer
cannot produce.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All three tickets RB-31 to RB-33 merged, one commit per ticket. RB-31 found a
genuine ADR-0006 violation: two registratie-wizard tests asserted a state the
real reducer cannot produce. RB-32 closed ADR-0003's own predicted failure
mode with a permanent CI drift guard rather than a one-time fix. RB-33 chose
deletion over adoption for an unused test helper, since manufacturing a first
caller would have removed no real duplication.
This closes the CD implementation phase. All 33 code tickets and the four
gated ADR-fixes are merged; npm run ci is green after every merge in this
arc, each verified independently rather than trusting an agent's own report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-C-010: intake, registratie-wizard, besluit and brief machine specs
hand-rolled a state literal, three of them hardcoding errors: {} by hand
instead of running real Msgs through the real reduce (ADR-0006 §2). intake
now shares the existing intake.testing.ts with the acceptance spec instead
of ignoring it; the other three each get a *.testing.ts one-liner.
Found real drift: two registratie-wizard tests asserted a cursor-2 state
reached before any diploma was chosen, which the real reducer cannot
produce (advancing past beroep requires a diploma already set). Replayed at
cursor 1 instead; submit() validates the whole draft regardless of cursor,
so the assertions are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
Four machine specs built their starting state with a hand-rolled object
literal instead of replaying real Msgs through the real reduce, the
exact anti-pattern ADR-0006 section 2 forbids. Three of the four also
hardcoded errors: {}, a shape the reducer might never actually produce.
intake.machine.spec.ts now imports the existing givenIntake from
intake.testing.ts (previously used only by intake.acceptance.spec.ts).
Three new one-line *.testing.ts files export the same given(reduce,
initial) wrapper for registratie-wizard, besluit, and brief. Every old
literal helper (answering, invullen, editingWith, loaded) is replaced
by a message replay that reaches the same state.
Two tests in registratie-wizard.machine.spec.ts asserted a cursor value
the real reducer cannot reach (cursor 2 with no diploma chosen yet,
which requires a diploma to already be set). Both are re-pointed at the
reachable cursor-1 equivalent; submit() validates the whole draft
regardless of cursor, so no assertion changed. Recorded in
implementation/rb-31.md, not worked around.
No *.machine.ts reducer was touched. All four specs pass; npm run ci
is green (lint, typecheck, dep:check, format, tokens, seam, all four
test suites, both app builds, backend 293/293, api-client drift).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-C-008: 9 files carried a CIBG-GAP EXTENSION marker against 8 register
rows. language-switcher had a well-formed marker and no row. Adds the row,
plus an optional ~14-line drift guard in check-tokens.sh that diffs the
marker set against the register and fails naming any missing row, so this
drift cannot silently reoccur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
The register at libs/shared/docs/cibg-gaps.mdx had 8 rows for 9
CIBG-GAP EXTENSION markers in code. language-switcher carries a
well-formed marker with no matching row, exactly as ADR-C-008 and
adr-c-007.md's handoff note flag. Add the row from the component's
own marker comment.
Also add a small guard to check-tokens.sh (folded into check:tokens,
as ADR-C-008 suggests as an optional step): it diffs the marker set
in code against the register's rows and fails CI on drift. Verified
working with a scratch marker file before removing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-C-011: unwrapOk had zero consumers anywhere in apps/ or libs/ since it
shipped, and the one candidate call site (submit-change-request.spec.ts's
inline parse-and-throw guard) already satisfies ADR-0006 section 3's real
requirement, never a cast. Manufacturing a first caller to justify keeping
the helper would remove no real duplication. Deleted rather than adopted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
unwrapOk had zero consumers in apps/ or libs/ since ADR-0006 shipped it.
The one call site the finding named already satisfies the ADR's real
rule (call the real parser, never a cast) with an inline guard, so
adding a manufactured first caller was not the better fix. This commit
deletes the helper and its file, and updates the one doc sentence that
named it. The finding's call site is unchanged. See rb-33.md for the
full adopt-or-delete reasoning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All seven tickets RB-24 to RB-30 merged, one commit per ticket. Records the
actual wave split, since the backlog's own depends-on column missed that
RB-24 rewrites imports in two of RB-28's target files.
RB-24 expanded its own scope to fix a second, real boundary violation that
deleting its acceptance criterion exposed, reviewed and accepted. Two more
findings were shown stale or overstated, on top of the nine from earlier
batches. RB-26 and RB-27 both correctly declined part of their own ticket's
proposed shape.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TE-005: xhrUpload buried the 2xx-vs-not check, JSON.parse-with-fallback and
ProblemDetails mapping inside XHR listener bodies, unreachable without
stubbing the XHR global. uploadOutcome(status, responseText) is now a pure
function with no DOM and no XHR stub in its spec. Abort-vs-error
disambiguation stays where it is: it fires on a different event with no
status or responseText, so it cannot fit the extracted signature. The
optional currentScenario() move into KeepaliveTransport.send() was not
taken, since it would cross into upload-shell.service.ts, outside this
ticket's scope.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
UploadAdapter.xhrUpload built new XMLHttpRequest() directly and put the
actual decisions inside its load listener: 2xx-vs-not, JSON.parse of the
body with a fallback, and ProblemDetails mapping via parseError. None of
it was reachable without stubbing the XHR global, so it had no spec
(TE-005; file LH 5/64, BRH 3/57).
Extract uploadOutcome(status, responseText): Result<string, {
documentId }>, a pure function next to genericError/parseError. It holds
the 2xx check, the JSON.parse-with-fallback, and the ProblemDetails
mapping. The load listener is now a two-line dispatch into it.
Abort-vs-error disambiguation stays where it is: it decides whether a
response exists at all, before uploadOutcome would even run, and the
proposed signature has no field for "aborted". It is already a one-line
ternary with no DOM-only logic to extract.
Add upload.adapter.spec.ts: plain describe/it, no DOM, no XHR stub,
covering a 2xx success, a 2xx unparseable body, a non-2xx ProblemDetails
body, a non-2xx non-ProblemDetails body, and the 200/300 boundary.
Verified red by editing uploadOutcome down to one line (an Edit, not
git checkout): 4 of 5 new specs failed. Re-applied with a second Edit.
Coverage for upload.adapter.ts: LH 5/64 -> 12/65, BRH 3/57 -> 7/59.
Skip TE-005's optional half (moving the currentScenario() branch into
KeepaliveTransport.send()): it needs a second file, upload-shell.
service.ts, and this ticket's own scope fences it to upload.adapter.ts
and its spec. The dev simulator's behaviour is unchanged.
Mark RB-27 implemented in 99-backlog.md and add its implementation note,
including a batch 5 close-out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TE-006: StamdataStore.download(), BriefStore.previewLetter() and
OrgTemplateStore.proefbrief() each ended in raw DOM blob calls jsdom cannot
meaningfully execute, so their success paths were unassertable and
download()'s two-clause guard true-branch was permanently dark.
BLOB_PRESENTER mirrors the SESSION_PORT shape; all three commands go through
it. download()'s branch coverage goes from 40.5% to 67.6%, and
org-template.store.ts gets its first spec at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
Three application-layer commands ended in raw DOM calls (URL.createObjectURL,
window.open, document.createElement('a').click(), URL.revokeObjectURL) as
their last statement. jsdom cannot assert a call that is also the end of the
function, so each command's success path stayed unassertable, and
StamdataStore.download()'s two-clause guard stayed permanently dark on its
true branch (TE-006).
Add BLOB_PRESENTER (libs/shared/src/application/blob-presenter.ts), an
InjectionToken mirroring SESSION_PORT's shape: an interface with open()/
download(), a real implementation preserving the existing open()-never-
revokes vs download()-always-revokes asymmetry, provided in root. Route
StamdataStore.download(), BriefStore.previewLetter(), and
OrgTemplateStore.proefbrief() through it.
Add specs with a recording fake presenter: StamdataStore.download()'s guard
(both clauses) and its success path, asserting toJson(...)'s exact output
reaches the file; BriefStore.previewLetter()'s existing success test now
goes through the seam instead of spying on window/URL directly; a new
org-template.store.spec.ts (none existed before) covers proefbrief()'s
success and failure paths.
Verified red without the fix by editing the download() filename to the
wrong extension, watching the success-path spec fail, then restoring it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TE-003: UploadShellService documented UploadTransport as the swap seam, then
bound the concrete, unexported KeepaliveTransport class directly, so a spec
could not fake it. UPLOAD_TRANSPORT copies the SESSION_PORT shape; the
default factory returns the same instance, so runtime behaviour is
unchanged. upload-shell.service.ts goes from 0% to 88.57% line coverage
across 16 new specs for upload(), cancel(), delete() and pollReturning().
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
UploadShellService injected the concrete KeepaliveTransport class instead
of a token. The class was not exported, so a spec could not fake it, and
could not provide against the UploadTransport interface either, since an
interface is not a DI token. The port existed only on paper.
Add UPLOAD_TRANSPORT, an InjectionToken with a default factory that
resolves the same KeepaliveTransport singleton, following the
SessionPort/SESSION_PORT shape. UploadShellService now injects the token.
Runtime behaviour is unchanged.
Add upload-shell.service.spec.ts: a recording fake transport plus a fake
UploadAdapter exercise upload(), delete(), cancel() and pollReturning(),
the four methods the missing seam left unreachable. Coverage for
upload-shell.service.ts goes from 0% to 88.6% line / 85% branch.
Mark RB-25 done in 99-backlog.md and add its implementation note.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TE-004: createUploadController performed three inject() calls, an effect()
registration and a window listener before returning, so the real policy
buried inside it — deciding per file whether to reject or start an upload —
was reachable only through a TestBed. planFileSelection in upload.machine.ts
is now that decision as a pure function taking plain {name, type, size}
objects; the controller executes the plan and keeps the one impure step
(crypto.randomUUID()) it can't move. No change to the controller's public
surface.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
createUploadController required inject(), an effect(), and a window listener
before a test could reach it. The file-selection policy trapped behind that
cost now lives in a pure function, planFileSelection, in upload.machine.ts.
planFileSelection takes plain { name, type, size } objects, not File, and
decides per file whether to reject it or accept it, with no I/O. The
controller executes the plan: it dispatches a rejection as-is, and starts the
upload for an accepted file (the one step that needs crypto.randomUUID()).
A new spec covers the three outcomes: the 'multiple' batch rejection, a
rejectReason-based rejection, and the accept case, plus order in a mixed
batch. Verified red-then-green with a temporary stub, undone by a second edit.
No change to the controller's public surface or to the calling organism.
previewUrlFor (added by RB-24) is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TE-008: five guard decisions in BriefStore (Save, Submit, Send, the shared
Approve/Reject review path) were pure functions of status tag, actor role and
entity completeness, but each sat inside a lock-held, DB-opening method, so a
spec could not exercise a decision without a booted host and a real SQLite
file. BriefRules.cs holds the five pure statics; BriefStore keeps its lock,
its Db.Create(), its static shape and every method signature. 29 new
free-running unit assertions in BriefRuleTests.cs; the existing host-booting
brief endpoint tests are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
TE-007: Render already accepts the letter's instant and uses it correctly for
the letterhead, but the body's datum placeholder resolved through ResolveAuto,
which ignored at and read DateTimeOffset.UtcNow. Threaded at through
RenderParagraphs and RenderNode, both already in Render's call chain with at
in scope. Zero public API change, zero call-site change. The bug this
prevents: re-rendering an archive or back-dating a letter would otherwise make
the letterhead and body dates disagree within a single document.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
LetterHtml.Render already receives the letter's instant and uses it
for the letterhead date. The body's "datum" placeholder resolved
through ResolveAuto, which ignored that instant and read the wall
clock instead. This is not a shipped bug today, because every current
caller passes Now() at render time. It becomes one the moment Render
runs with a historical instant (an archive re-render, a back-dated
letter): the letterhead and the body would then disagree within one
document.
Thread the existing "at" parameter down through RenderParagraphs and
RenderNode into ResolveAuto's "datum" case. Render's own signature,
and every call site, stays unchanged.
Add two tests with a fixed historical "at": one pins the body's
rendered date to the expected Dutch string, the other asserts the
letterhead date and the body date agree. Both fail red against the
old code, showing today's date instead of the pinned one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BriefStore's five guard decisions (Save, Submit, Send, and the shared
Approve/Reject review path) were pure functions of status tag, actor role,
and entity completeness, but each sat inside a lock-held, DB-opening
method. A spec could not exercise the decision without a booted host and
a real SQLite file.
Extract the guards into a pure Domain/Letters/BriefRules.cs. BriefStore
keeps its lock, its Db.Create(), its static shape, and every method
signature — only the if cascades move. Add BriefRuleTests.cs (29
assertions, ~120 ms, no host boot) covering every branch, including the
rejected-to-draft reopen on save, the required-filled gate on submit,
and the non-drafter and self-review denials. The existing host-booting
brief endpoint tests are unchanged and still pass, proving the
extraction preserved behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-C-002: libs/shared/src/upload/ held a network adapter outside
infrastructure/ and the only Elm machine outside a domain/ folder, and the
dependency-cruiser rule was written around the violation rather than the
violation being fixed. The five files move to infrastructure/, domain/ and
application/, and the ^libs/shared/src/upload/ carve-out is gone.
Deleting the carve-out exposed a second, real violation that the old path had
hidden from the ui-not-infrastructure rule: three UI components injected
UploadAdapter for nothing but a one-line wrapper over its own exported pure
uploadContentUrl. They now read previewUrlFor from their application-layer
collaborator. dep:check passes for both apps with the clause removed, which is
the ticket's acceptance criterion.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
libs/shared/src/upload/ held a network adapter, an Elm machine, and two
application-layer coordinators outside the folder-per-layer convention every
other context follows. The dependency-cruiser rule carved an exception around
the misplaced adapter instead of the violation being fixed.
Move all five files to the layer each belongs to (git mv), update every
import across 24 consumer files, then delete the carve-out clause from
.dependency-cruiser.base.js. No export renamed, no file split, no spec
content changed.
Deleting the carve-out exposed a second, pre-existing rule violation:
ui-not-infrastructure had never fired against upload.adapter.ts because its
old path did not match /infrastructure/. Three UI components injected
UploadAdapter directly for its one-line contentUrl() wrapper. Route each
through the existing pure uploadContentUrl() function via the application
layer (upload-controller's new previewUrlFor, OrgTemplateStore's new
previewUrlFor) instead — the same idiom brief.store.ts already used.
npm run ci passes; dep:check is clean for both apps with the carve-out gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All six tickets RB-18 to RB-23 merged, one commit per ticket. Records the two
incomplete tickets that the agents reported, RB-22's deliberate departure from
the runResult idiom, and how RB-19 was verified as a pure reorder.
Adds five dispatch lessons. The stale worktree base is now the rule at 11 of 13
agent-runs. A spend limit killed four agents mid-flight and a message resumed
each one from its own transcript, so no work was redone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-006: the file declared direction as its organising principle, then switched
to feature grouping without saying so, and five sections interleaved reads and
writes. Each section now orders reads first, with the WP-65 sub-banner pair.
DELETE /admin/cases/{id} and GET /admin/audit move up beside GET /admin/cases,
129 lines closer. The org-template preview moves to the org-template section.
Pure reordering. Verified centrally: the sorted list of all 47 route strings is
identical before and after, and so is every (route, .Gate marker, wrapper called
in the handler) triple. The swagger.json and api-client.ts diffs are ordering
only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-006 found that Program.cs states a reads-then-writes principle at the
top of the file, then abandons it for five feature sections that mix GET
and mutating endpoints in mapping order. This is a pure reorder: within
Document upload, Applications, Admin cases, Brief, and Organization
templates, every GET now precedes every POST/PUT/DELETE, each split by a
`--- reads ---`/`--- writes ---` sub-banner in the style WP-65 already
established for Beoordeling/Besluit.
DELETE /admin/cases/{id} and GET /admin/audit move up beside GET
/admin/cases, closing the 129-line gap CQ-006 measured. GET
/admin/org-template/{subOrgId}/preview moves from the Brief section to
the Organization-templates section it actually belongs to.
No route, signature, DTO, or handler body changed. Every block was cut
by exact line-range slicing, never retyped. The sorted list of mapped
HTTP-method-plus-path strings is byte-identical before and after; every
.Gate(...) count is unchanged; the three routes that moved with a gate
were checked by eye against the wrapper their handler actually calls,
per RB-12's stated limitation that the route-table test only proves a
marker is present, not that it still matches the handler.
npm run gen:api regenerated backend/swagger.json and
libs/shared/src/infrastructure/api-client.ts; both diffs are ordering
only (sorted-file diff is empty), committed alongside per the ticket's
own guidance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-007 contract half. GET /brief was the only backend endpoint where a GET
performed a persisted write, and the FE retries GETs automatically, so a
transient failure could enter the create path. BriefStore.GetOrCreate splits
into Get plus the existing ResetAndCreate. GET /brief/preview shared the same
call site and gets the same treatment. RB-22 already made the FE tolerate the
404, so the pair is complete.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /brief allocated a row on first call (BriefStore.GetOrCreate) — the
one endpoint in the backend where a read performed a persisted write.
The FE retries GETs automatically, so a transient failure could enter
the create path more than once; a lock prevented a duplicate row, but
the safety depended on the lock, not on the endpoint being a query.
Split GetOrCreate into Get (a pure query) and the already-existing
ResetAndCreate (POST /brief/reset owns creation). GET /brief now 404s
when the owner has no brief yet. GET /brief/preview used GetOrCreate
too, so it gets the same Get + 404 treatment, forced by the split.
RB-22 already made BriefStore.load() on the FE tolerate a 404 by
calling reset() once; this ticket is what makes that branch live.
Updated the brief/preview/org-template backend tests that assumed
GET seeded a brief on first call to create one explicitly first, and
added a test that GET 404s and writes no row without the fix (verified
red beforehand). Regenerated the API client (npm run gen:api).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-007 expand half. BriefStore.load() treats a 404 as 'no brief yet' and calls
the existing reset() command once. load()'s error channel becomes the
BriefLoadFailure union, because runResult folds the HTTP status away and the
store needs it. Today's backend never 404s, so the branch is a no-op until
RB-23 lands the contract half.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
BriefStore.load() now treats a 404 from GET /brief as "no brief exists
yet" and calls the existing reset() command once, instead of showing
the generic load-failed error. BriefAdapter.load() gains a
BriefLoadFailure error channel (notFound | error) so the store can
tell a 404 apart from every other failure; every other adapter method
stays on runSubmit, unchanged.
The once-only bound is a field on the store, not a comment: a second
404 (from a later load() call) always falls through to the ordinary
error path, and the recovery path never calls load() again, so no
loop can form.
This is the expand half of CQ-007's split (04-cqrs-light.md). Today's
backend never 404s GET /brief, so the new branch is dead code until
RB-23 (the backend contract half) ships in a later merge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-002: ApplicationsStore.cancel and AdminCasesStore.delete reached the raw
ApiClient and swallowed the failure in a bare catch, so a failed cancel made the
row reappear with no message. Both now fold through runSubmit and expose
lastError, which the two pages render.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
BIO-018: the store was a process-global dictionary keyed on the client-supplied
Idempotency-Key alone, so one caller could replay another caller's key and
receive their cached response. The key is now scoped with the caller SubjectId.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
# docs/project/refactor-backlog-setup/refactor-backlog/99-backlog.md
# libs/shared/docs/behaviour-spec.mdx
ApplicationsStore.cancel and AdminCasesStore.delete rolled an optimistic
write back on failure but showed no message — a bare catch with no
Result and no error channel (CQ-002). Both now call runSubmit and set a
lastError signal on failure, mirroring createSubmitChangeRequest in the
same folder. Each page renders the error with the existing app-alert
atom, the same pattern brief.page.ts already uses for lastError.
Added a spec file for ApplicationsStore (none existed) and extended
AdminCasesStore's spec, each asserting the rollback AND the surfaced
error. Verified both new assertions fail without the fix (an Edit
undo/redo of the store method, not git checkout).
Regenerated libs/shared/docs/behaviour-spec.mdx (gen:behaviour-spec) to
pick up the new/renamed test names. Marked RB-20 done in 99-backlog.md
and recorded the change in implementation/rb-20.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CQ-001: createDraftSync was the longest function in the repo and owned three
query paths next to its write path. findConcept and loadConcept are now free
functions that take the adapter, so they have a direct spec without TestBed.
The closure state (id, ensuring, resumeGate) stays where it was, because the
coupling is load-bearing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The architect approved the four ADR-fix tickets. All four change what the
architecture documents claim. No code changes.
ADR-0001, ADR-C-001: the worked example claimed the POC has no real backend.
It rewrites against `backend/src/BigRegister.Api`. Every path it named is
repointed. The out-of-scope list drops two discharged bullets: 33 `parse*`
boundaries exist, and `npm run gen:api` is real.
ADR-0001, ADR-C-003: a new section states that the generated client is the wire
contract. A hand-written `contracts/*.dto.ts` is the exception for two cases
only. The four survivors stay, because NSwag emits every property as optional
and flattens `RegistrationStatusDto` into five optional strings. The `parse*`
trust boundary stays mandatory, because a generated type is a compile-time
claim about the wire and not a runtime guarantee.
ADR-0003, ADR-C-007: four paths moved in WP-67 and are repointed. Point 4 kept
the principle and changed its example to `skeleton` and `spinner`. Two of its
claims were false and the amendment says so: `app-alert` wraps the vendored
`.feedback` classes, and `site-header` composes the vendored `.titlebar`.
ADR-0004, ADR-C-009: the exception section states a four-part test instead of
one named exception. `OrgTemplateStore` and `FeatureFlagStore` both pass it. RB-07
gated this ticket, because clause 4 needs an audited allow path. RB-07 landed
that, so the ADR does not ratify a control that the code lacks.
Three tickets need a matching CLAUDE.md correction in the same diff. CLAUDE.md
section 2 loses the false `alert` example. Section 4 gets the generated-client
rule and the four-part test.
Two findings were wrong. ADR-C-001 asked to keep an out-of-scope bullet that
reads "SessionStore is in-memory". The session persists to `localStorage` now,
so the bullet covers multi-tab sync only. ADR-C-007 flagged one half of point 4
and missed that the other half is equally false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>