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>
11 KiB
Readable codebase — apply the dashboard pattern to the rest of the app
The dashboard refactor cut dashboard.page.ts from 340 lines to 42. It was built as a
reference implementation: prove the pattern on one screen, then hold the rest of the app
to the budget it establishes.
This arc applies that result. The full design record, with every measurement and every
rejected alternative, is PLAN.md. Each ticket below is one commit.
Two findings shape the work:
- Pages are already thin (17 pages, median 88 lines). The remaining bulk sits one layer down, in organisms.
- The worst problem is not size.
runIfSubmittingis copy-pasted into 5 components and must be called by hand afterdispatch. Forgetting it fails silently. RD-06 fixes two user-facing bugs that follow from it.
Session protocol
- One ticket per session. Read
CLAUDE.md, this README, the ticket file, and the ticket's "Read first" list — then execute. Do not start the next ticket in the same session. - The Decisions block in each ticket is pre-made. Do not relitigate it.
PLAN.mdrecords why, including the alternatives that were rejected and the reasons. - Ticket files are written just in time, not all 35 up front. Writing one means choosing
which decisions and which traps from
PLAN.mdbelong in it, so the supervisor writes the ticket file (an Opus-shaped job) immediately before delegating it. The file lands in that ticket's own commit. Generating all 35 in advance would be speculative — later tickets are better written once the earlier ones have taught us something. - Match the model to the step (see CLAUDE.md, "Model routing for agent delegation").
Executing a ticket is written for the
developeragent (Sonnet). Read-only checks go totask-runner(Haiku). Escalate toplanner(Opus) only if a Decisions block turns out to be wrong — in which case stop, and fixPLAN.mdfirst. - A ticket ends GREEN, with its acceptance criteria checked, its
Status:set todone, and its README row updated — all in the same commit as the code. Never in a follow-up commit. This is what makes a restart safe: whatever is committed is done, and whatever is not is not. Status: donecarries no commit hash, because a commit cannot contain its own hash. The commit is recoverable when you need it:git log --oneline --diff-filter=A -- docs/project/readable-codebase/RD-NN-*.md.- No ticket leaves a check disabled without an inline reason and a reference to the ticket that removes it.
GREEN (global definition of done)
npm run ci
For any ticket whose "--full?" column says yes — it touches a story, an .mdx, or
libs/shared/src/ui/** — additionally:
npm run ci --full
npm run ci does not build Storybook. Only --full does, and a broken .mdx story
import is invisible until it runs. RD-27 in particular must not be pushed without it.
Recovery after a restart
A fresh session with no context needs three commands:
git log --oneline -8
grep -rn '^Status:' docs/project/readable-codebase/RD-*.md | grep -v done # next work
npm run ci # is HEAD green?
Then read PLAN.md for the design record, and the first todo ticket for the work.
The agent loop
One supervisor session drives it; one developer agent executes each ticket:
- Read the Order table. Pick the first
todowhose dependencies are alldone. - Spawn one
developeragent: "ReadCLAUDE.md, thendocs/project/readable-codebase/README.md, thenRD-NN.mdand its Read-first list. Execute it. End GREEN. Update the ticket Status and the README row in the same commit as the code. Do not start another ticket." - Verify with
task-runner:npm run ci,git log -1 --stat, and thatStatus:now readsdone. Never mark a ticket done on an agent's report alone — the check is the exit code. - Green: next iteration. Red: stop and surface it.
Run tickets sequentially. Three properties make concurrent writes to one branch hostile:
behaviour-spec.mdx and snippets.generated.ts are regenerated and drift-checked, so two
agents regenerating conflict by construction; every ticket writes this README's Order table;
and the file sets overlap (the three wizards appear in RD-06, RD-07, RD-22 and RD-23).
Parallel work pays only for genuinely disjoint tickets, in separate git worktrees, merged deliberately — RD-18/RD-19 (the ticket sweep) and the Phase 5 doc tickets qualify. Cap at two. Note that RD-15 exists because 22 abandoned agent worktrees are still on disk.
Order
| ID | Ticket | Deps | --full? |
Status |
|---|---|---|---|---|
| RD-01 | Scaffold this backlog: README, PLAN, ticket template | — | done | |
| RD-02 | max-lines rule + reportUnusedDisableDirectives + 7 disables |
01 | done | |
| RD-03 | overzicht context: page + 2 nav sections, boundary edge, admin-links token |
02 | yes | done |
| RD-04 | Story titles to Domein/<Context>/<Name>; add the missing stories |
03 | yes | todo |
| RD-05 | createStore gains the effect map + specs |
02 | done | |
| RD-06 | Bug fix: 2 single-step forms to the effect map + retry affordance | 05 | yes | done |
| RD-07 | Add Primary to the 3 wizard machines + specs |
05 | done | |
| RD-08 | Migrate the 3 wizards to the effect map + Primary |
07 | yes | done |
| RD-09 | Docs + generator: form-machine.hbs, ARCHITECTURE, fp-tea, skill |
08 | todo | |
| RD-10 | WizardStatus to a payload-carrying WizardPhase |
08 | yes | todo |
| RD-11 | Fold the lifecycle projection into remote-data.ts; PascalCase 3 machines |
01 | todo | |
| RD-12 | ActionState becomes action on BriefState.Loaded |
11 | todo | |
| RD-13 | Same for org-template, folding pendingPublish in |
12 | todo | |
| RD-14 | Move SaveState to debounced-save.ts; delete action-state.ts |
13 | todo | |
| RD-15 | Remove 22 abandoned agent worktrees (4.7 GB) | 01 | todo | |
| RD-16 | parseDashboardView returns BigProfile; delete DashboardView |
01 | todo | |
| RD-17 | successOf/successOr sweep — 10 sites, 8 files |
01 | todo | |
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | todo | |
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | todo | |
| RD-20 | wizard-errors.ts + spec, adopted by all 3 wizards |
02 | todo | |
| RD-21 | rich-text-dom.ts helpers + spec cases |
02 | yes | todo |
| RD-22 | intake-wizard to 3 step components |
08, 20 | yes | todo |
| RD-23 | registratie-wizard to 3 steps + the upload-controller move |
08, 20 | yes | todo |
| RD-24 | concepts.page to 6 sections + concept-card + globals + code tokens |
02 | yes | todo |
| RD-25 | org-template-editor to sample-letter.ts + labels + 2 children |
02 | yes | todo |
| RD-26 | letter-canvas: inline the labels + letter-line; keep one disable |
02 | yes | todo |
| RD-27 | The layer move: 33 git mv + 28 specifiers + 8 MDX imports |
21 | yes | todo |
| RD-28 | Layer-tag fixes + the libs/beheer title rule |
27 | todo | |
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | todo | |
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | todo | |
| RD-31 | ARCHITECTURE.md section 6a: symbols not lines, 2 dead paths, new names |
03, 08, 16 | todo | |
| RD-32 | fp-tea-atomic-design.md: 11 broken paths + the broken anchor |
27 | todo | |
| RD-33 | CLAUDE.md + atomic-design.mdx + the ui-component skill |
03, 27, 29 | yes | todo |
| RD-34 | (optional) NO_SUBORGS/NO_TABLES become RemoteData.Empty |
11 | todo | |
| RD-35 | (optional, last, alone) upload type: discriminant to tag: |
27 | todo |
The ID order already respects every dependency, so it is the recommended running order.
Independent tickets. RD-15 through RD-19 depend only on RD-01. Pull them forward to fill a short session. Take RD-15 early: it makes every later repository search faster.
Two ordering traps the table encodes. RD-01 must precede RD-30, because RD-01 copies its ticket template out of the directory that RD-30 archives. And four tickets edit the same two documents in different sections — RD-09 rewrites the submit-idiom teaching, while RD-31 and RD-32 fix section 6a and the stale paths. Sequential is fine. Never put those pairs in parallel worktrees.
Ticket template
# RD-NN — Title
Status: todo | in-progress | done
Source: PLAN.md section <n>
## Why
## Read first
## Decisions (pre-made, don't relitigate)
## Files
## Steps
## Acceptance criteria
## Verification
## Out of scope
## Risks
Three rules when you write a ticket file, because the agent reads its ticket and not
PLAN.md:
-
Copy the decision, never a pointer to it. The verdict goes in the Decisions block, verbatim.
-
Inline the traps that apply to that ticket. A trap recorded only in
PLAN.md's global Risks section is a trap that fires. -
State acceptance as a command, not a sentence. "Lands about 230 lines" is a design estimate and nothing can check it.
npm run linthas an exit code. -
Run every acceptance command against the tree before you hand the ticket over. A command that cannot pass is worse than no command: the agent either wastes a cycle or, worse, "fixes" correct code to satisfy it. Three real misses so far, all in tickets written by the supervisor:
- RD-06 grepped only
runIfSubmitting, missing that one wizard spells itrunIfIndienen. - RD-08 grepped bare
onPrimary\|onRetry, which can never return nothing — an unrelateduploadCtl.onRetryexists inupload-controller.ts. - RD-08 said "no machine changes" while also requiring a repo-wide grep to come back clean, which forced comment edits in three machines. The two instructions contradicted each other.
Anchor greps on a declaration (
^ onRetry\(\)) rather than a name, and make the Files list agree with the Acceptance commands. - RD-06 grepped only