405 Commits
Author SHA1 Message Date
ehoandClaude Opus 5 1866947438 chore(deps): pin Angular to 22.0.5 and gate audit at high
CI / changes (push) Successful in 7s
CI / lint (push) Successful in 1m57s
CI / frontend (push) Successful in 2m45s
CI / backend (push) Successful in 1m57s
CI / e2e (push) Failing after 4m10s
CI / semgrep (push) Successful in 1m18s
CI / api-client-drift (push) Successful in 2m9s
CI / storybook-a11y (push) Successful in 11m5s
Angular 22.1.x emits `var(--%NS%name)` for every CSS custom property in a
component `styles:` block. No `@angular/core` release substitutes the
placeholder, so all `--rhc-*` tokens resolve to nothing and the UI breaks.
`npm run ci` does not catch it; only the Storybook axe job does.

Pin every `@angular*` entry to the exact version 22.0.5, so a plain
`npm install` cannot pull 22.1.x back in.

Holding at 22.0.5 leaves three moderate advisories open, which made the audit
step fail: GHSA-p297-fm68-3q8c and GHSA-hh8m-fm6v-7cvg. Neither is reachable.
The app calls no `withRequestsMadeViaParent` and no `provideClientHydration`,
and binds no untrusted value into a directive host binding. The audit gate
therefore runs at `--audit-level=high`. A high advisory still fails the build.

Restore the default audit level together with the upgrade, after an Angular
release substitutes the placeholder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 13:53:05 +02:00
ehoandClaude Opus 5 6330773fd5 chore: add a Taskfile facade over the existing commands
CI / changes (push) Successful in 16s
CI / lint (push) Successful in 3m0s
CI / frontend (push) Failing after 3m36s
CI / backend (push) Successful in 2m38s
CI / e2e (push) Failing after 4m14s
CI / storybook-a11y (push) Failing after 7m50s
CI / semgrep (push) Successful in 1m18s
CI / api-client-drift (push) Successful in 1m55s
`task` with no arguments lists every runnable command. The Taskfile calls the
npm scripts, dotnet and docker compose. It does not duplicate their logic.
CI does not need `task`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 08:56:39 +02:00
ehoandClaude Opus 5 856463b738 Merge refactor/machine-wiring-in-application — RD-30..33, RD-36, RD-37, RD-39
Closes the readable-codebase arc's phase 5. RD-37 removed the last five axe
suppressions by making the keuzelijst row the <li>. RD-30 archived the two
finished backlogs. RD-31, RD-32, RD-33 and RD-36 repaired the documents that
describe the refactored tree.

RD-34 and RD-35 stay open. Both are marked optional in the backlog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 16:21:30 +02:00
ehoandClaude Opus 5 44dcc69811 refactor: ui/dashboard becomes ui/overzicht-secties (RD-36)
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>
2026-09-09 16:21:15 +02:00
ehoandClaude Opus 5 d9aef9541f docs: teach overzicht, max-lines and the step contract (RD-33)
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>
2026-09-09 16:14:56 +02:00
ehoandClaude Opus 5 682db41344 docs: repoint fp-tea paths at the monorepo tree (RD-32)
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>
2026-09-08 23:16:26 +02:00
ehoandClaude Opus 5 241fe1b57b docs: cite symbols, not lines, in ARCHITECTURE 2g
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>
2026-09-08 23:12:56 +02:00
ehoandClaude Opus 5 d31e054504 docs: cite symbols, not lines, in ARCHITECTURE 6a (RD-31)
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>
2026-09-08 23:11:03 +02:00
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00
ehoandClaude Opus 5 097e8468e0 fix: keuzelijst rows become the <li> (RD-37)
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>
2026-09-08 22:51:04 +02:00
ehoandClaude Opus 5 2aa343f255 chore: cap container memory in docker compose
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>
2026-09-08 22:39:40 +02:00
ehoandClaude Opus 5 f3e5745145 fix: the wizards' seed input never arrived (RD-39)
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>
2026-09-07 11:53:03 +02:00
eho e6bc19c790 Merge refactor/readable-codebase — RD-01..RD-29 + RD-37/38
CI / changes (push) Successful in 10s
CI / lint (push) Successful in 1m14s
CI / frontend (push) Successful in 2m15s
CI / backend (push) Successful in 2m7s
CI / e2e (push) Failing after 3m37s
CI / semgrep (push) Successful in 1m4s
CI / api-client-drift (push) Successful in 1m45s
CI / storybook-a11y (push) Successful in 10m43s
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.
2026-09-06 07:11:44 +02:00
ehoandClaude Opus 5 fc2a3c348b refactor: one member order for the 3 wizard containers (RD-38)
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>
2026-09-05 20:16:23 +02:00
ehoandClaude Opus 5 551cabce5e docs: add RD-37 — five a11y suppressions name a ticket that closed
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>
2026-09-05 08:37:26 +02:00
ehoandClaude Sonnet 5 b6bd7eea5a refactor: enforce the atomic ladder in dependency-cruiser (RD-29)
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>
2026-09-05 08:32:52 +02:00
ehoandClaude Sonnet 5 d4c5a9450a fix: layer-tag mismatches + libs/beheer title rule (RD-28)
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>
2026-09-05 08:26:47 +02:00
ehoandClaude Opus 5 57fc72f254 docs: correct RD-27's occurrence count, the fourth time in one pattern
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>
2026-09-05 08:15:20 +02:00
ehoandClaude Sonnet 5 43dc3210cd refactor: move libs/shared/src/ui/ into atoms/molecules/organisms (RD-27)
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>
2026-09-05 08:14:10 +02:00
ehoandClaude Sonnet 5 7a8eab917b refactor: inline letter-canvas's 19 pure labels, extract letter-line (RD-26)
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>
2026-09-05 00:25:35 +02:00
ehoandClaude Opus 5 bcac3789d3 docs: fix RD-25's decision 4, and name the pattern behind three misses
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>
2026-09-05 00:03:30 +02:00
ehoandClaude Sonnet 5 cf1f641534 refactor: split org-template-editor by output cluster (RD-25)
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>
2026-09-05 00:02:30 +02:00
ehoandClaude Opus 5 5aed15bb98 docs: record the supervisor-as-second-writer mistake from RD-24
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>
2026-09-04 23:44:17 +02:00
ehoandClaude Sonnet 5 630d68045f refactor: split concepts.page into 6 sections, fix dead highlighting (RD-24)
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>
2026-09-04 23:44:01 +02:00
ehoandClaude Opus 5 d5a7a25a78 docs: record RD-23's two acceptance misses as recurrences
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>
2026-09-04 23:18:26 +02:00
ehoandClaude Sonnet 5 11e3191099 refactor: split registratie-wizard into three steps (RD-23)
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>
2026-09-04 23:17:25 +02:00
ehoandClaude Sonnet 5 8e1de38c68 refactor: split intake-wizard into three step components (RD-22)
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>
2026-09-04 22:58:52 +02:00
ehoandClaude Sonnet 5 4b3e6a6cfd refactor: move selection surgery into rich-text-dom.ts (RD-21)
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>
2026-09-04 22:42:21 +02:00
ehoandClaude Opus 5 8a3e42015b docs: add RD-36 — rename the stale ui/dashboard/ folder
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>
2026-09-04 22:31:55 +02:00
ehoandClaude Opus 5 dff5f96bb3 docs: correct RD-20's file count, and record the ninth miss
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>
2026-09-04 22:01:01 +02:00
ehoandClaude Sonnet 5 831940f1b9 refactor: extract toWizardErrors, adopted by all 3 wizards (RD-20)
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>
2026-09-04 22:00:04 +02:00
ehoandClaude Opus 5 a196a380ce docs: restore the two backlog paths RD-19's target of 0 removed
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>
2026-09-04 21:50:06 +02:00
ehoandClaude Sonnet 5 8560746d15 refactor: strip WP-/RB- ticket refs from backend (RD-19)
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>
2026-09-04 21:48:08 +02:00
ehoandClaude Sonnet 5 dd11eafe50 refactor: strip WP-/RB- ticket refs from apps and libs (RD-18)
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>
2026-09-04 21:23:07 +02:00
ehoandClaude Sonnet 5 3895588b9a refactor: story titles to Domein/<Context>/<Name>; add 2 missing stories (RD-04)
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>
2026-09-04 20:56:17 +02:00
ehoandClaude Opus 5 fb2e58ab87 docs: record the concurrent-agent trap that RD-17 hit
`/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>
2026-09-04 20:41:32 +02:00
ehoandClaude Sonnet 5 e221834f6e refactor: add successOr, sweep remaining inline unwraps (RD-17)
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>
2026-09-04 20:33:56 +02:00
ehoandClaude Opus 5 c36d9e3ff0 docs: drop RD-16 — the instruction would have discarded server decisions
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>
2026-09-04 18:50:38 +02:00
ehoandClaude Sonnet 5 fb7b531fdf chore: remove 22 abandoned agent worktrees (RD-15)
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>
2026-09-04 18:48:52 +02:00
ehoandClaude Opus 5 c45d1bc0ff docs: git grep -c counts lines, not occurrences (the seventh miss)
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>
2026-09-04 18:46:00 +02:00
ehoandClaude Sonnet 5 a8c7a573fc refactor: move SaveState into debounced-save.ts, delete action-state.ts (RD-14)
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>
2026-09-04 18:45:04 +02:00
ehoandClaude Sonnet 5 c599fee8e2 refactor: fold org-template's action lifecycle + pendingPublish into one union (RD-13)
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>
2026-09-04 18:38:23 +02:00
ehoandClaude Opus 5 8e5f48c5d2 docs: scope acceptance checks to the ticket's files, not a directory
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>
2026-09-04 18:28:13 +02:00
ehoandClaude Sonnet 5 02d41536df refactor(brief): move the action lifecycle into the machine (RD-12)
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>
2026-09-04 18:27:09 +02:00
ehoandClaude Opus 5 43f62ddfee docs: record the fifth acceptance-check miss, and the stale SQLite trap
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>
2026-09-04 18:08:21 +02:00
ehoandClaude Sonnet 5 827c655c1b refactor: fold machine-remote-data into remote-data.ts, PascalCase load lifecycle (RD-11)
`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>
2026-09-04 18:07:00 +02:00
ehoandClaude Sonnet 5 11664d2efa refactor: WizardStatus to a payload-carrying WizardPhase (RD-10)
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>
2026-09-04 17:45:05 +02:00
ehoandClaude Opus 5 84cbf3f7d8 docs: use git grep for acceptance checks, and fix RD-09's ledger row
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>
2026-09-04 17:28:37 +02:00
ehoandClaude Sonnet 5 d15943bb36 docs: teach the effect map, not the deleted submit method (RD-09)
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>
2026-09-04 17:26:52 +02:00
ehoandClaude Sonnet 5 b8aced75e3 refactor: migrate the 3 wizards to the effect map and Primary (RD-08)
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>
2026-09-04 17:20:52 +02:00