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>
312 lines
19 KiB
Markdown
312 lines
19 KiB
Markdown
# 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`](PLAN.md). Each ticket below is one commit.
|
|
|
|
Two findings shape the work:
|
|
|
|
1. **Pages are already thin** (17 pages, median 88 lines). The remaining bulk sits one layer
|
|
down, in organisms.
|
|
2. **The worst problem is not size.** `runIfSubmitting` is copy-pasted into 5 components and
|
|
must be called by hand after `dispatch`. 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.md` records
|
|
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.md` belong 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 `developer` agent (Sonnet). Read-only checks go to
|
|
`task-runner` (Haiku). Escalate to `planner` (Opus) only if a Decisions block turns out to
|
|
be wrong — in which case stop, and fix `PLAN.md` first.
|
|
- A ticket ends **GREEN**, with its acceptance criteria checked, its `Status:` set to `done`,
|
|
**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: done` carries 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)
|
|
|
|
```bash
|
|
npm run ci
|
|
```
|
|
|
|
For any ticket whose "`--full`?" column says yes — it touches a story, an `.mdx`, or
|
|
`libs/shared/src/ui/**` — additionally:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. Read the Order table. Pick the first `todo` whose dependencies are all `done`.
|
|
2. Spawn **one** `developer` agent: _"Read `CLAUDE.md`, then
|
|
`docs/project/readable-codebase/README.md`, then `RD-NN.md` and 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."_
|
|
3. Verify with `task-runner`: `npm run ci`, `git log -1 --stat`, and that `Status:` now reads
|
|
`done`. Never mark a ticket done on an agent's report alone — the check is the exit code.
|
|
4. 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 | done |
|
|
| 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 | Teach the effect map: ARCHITECTURE §2d + fp-tea (2 docs, no generator) | 08 | | done |
|
|
| RD-10 | `WizardStatus` to a payload-carrying `WizardPhase` | 08 | yes | done |
|
|
| RD-11 | Fold the lifecycle projection into `remote-data.ts`; PascalCase 3 machines | 01 | | done |
|
|
| RD-12 | `ActionState` becomes `action` on `BriefState.Loaded` | 11 | | done |
|
|
| RD-13 | Same for org-template, folding `pendingPublish` in | 12 | | done |
|
|
| RD-14 | Move `SaveState` to `debounced-save.ts`; delete `action-state.ts` | 13 | | done |
|
|
| RD-15 | Remove 22 abandoned agent worktrees (4.7 GB) | 01 | | done |
|
|
| RD-16 | ~~`parseDashboardView` returns `BigProfile`~~ — DROPPED, see PLAN.md 2.2 | 01 | | n/a |
|
|
| RD-17 | `successOf`/`successOr` sweep — 10 sites, 8 files | 01 | | done |
|
|
| RD-18 | Ticket-reference sweep, frontend — 181 refs, 100 files | 01 | yes | done |
|
|
| RD-19 | Ticket-reference sweep, backend — 370 refs, 86 files | 01 | | done |
|
|
| RD-20 | `wizard-errors.ts` + spec, adopted by all 3 wizards | 02 | | done |
|
|
| RD-21 | `rich-text-dom.ts` helpers + spec cases | 02 | yes | done |
|
|
| RD-22 | `intake-wizard` to 3 step components | 08, 20 | yes | done |
|
|
| RD-23 | `registratie-wizard` to 3 steps + the upload-controller move | 08, 20 | yes | done |
|
|
| RD-24 | `concepts.page` to 6 sections + `concept-card` + globals + code tokens | 02 | yes | done |
|
|
| RD-25 | `org-template-editor` to `sample-letter.ts` + labels + 2 children | 02 | yes | done |
|
|
| 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 |
|
|
| RD-36 | `ui/dashboard/` → `ui/overzicht-secties/` + 2 stale `dashboard.page` paths | 04 | yes | 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.
|
|
|
|
**RD-36 is last by number, not by dependency.** It needs only RD-04, it is 8 `git mv`s plus
|
|
four import lines, and it collides with nothing else in the table — RD-27's move is confined to
|
|
`libs/shared/src/ui/`. Pull it forward into any short session. It is numbered last only because
|
|
it was added after RD-04 shipped.
|
|
|
|
**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
|
|
|
|
```markdown
|
|
# 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`:
|
|
|
|
1. **Copy the decision, never a pointer to it.** The verdict goes in the Decisions block,
|
|
verbatim.
|
|
2. **Inline the traps that apply to that ticket.** A trap recorded only in `PLAN.md`'s global
|
|
Risks section is a trap that fires.
|
|
3. **State acceptance as a command, not a sentence.** "Lands about 230 lines" is a design
|
|
estimate and nothing can check it. `npm run lint` has an exit code.
|
|
4. **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. Seven real misses so far, all in tickets written
|
|
by the supervisor:
|
|
- RD-06 grepped only `runIfSubmitting`, missing that one wizard spells it `runIfIndienen`.
|
|
- RD-08 grepped bare `onPrimary\|onRetry`, which can never return nothing — an unrelated
|
|
`uploadCtl.onRetry` exists in `upload-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. RD-23 repeated it exactly: its Decisions block mandated the line
|
|
`dispatch: (msg) => this.uploadMsg.emit(msg)` — the upload controller's own property name —
|
|
while its acceptance demanded zero occurrences of `dispatch` in the step files. **Grep the
|
|
text your own mandated snippet contains, and you have written a check that cannot pass.**
|
|
Anchor on what you actually forbid: here, `this.dispatch` or `store.dispatch`, not the bare
|
|
word.
|
|
- RD-09 grepped `docs/ apps/ libs/ .claude/`, which also matched this backlog's own ticket
|
|
files (they name the deleted method as the history of `done` work) and 22 gitignored
|
|
abandoned worktrees. Satisfying it literally would have corrupted completed-ticket
|
|
history.
|
|
- RD-11 asserted `git grep "machineRemoteData\|LoadLifecycle"` returns nothing, but the
|
|
replacement it mandates is named **`fromLoadLifecycle`** — which contains the old name as
|
|
a substring. The check can never pass. **When the new name contains the old one, anchor
|
|
on word boundaries**: `git grep -w machineRemoteData` and
|
|
`git grep -nE "(^|[^a-zA-Z])LoadLifecycle\b"`.
|
|
|
|
- RD-19 asserted the backend sweep must reach **0** references, after RD-18 had established
|
|
that a reference inside a path to a document that still exists is a pointer, not
|
|
provenance, and exempted one. The supervisor did not run that check against `backend/`.
|
|
Two such paths existed. The target of 0 gave the agent no way to keep them, so it
|
|
described the two documents in prose instead — losing a path that resolved. Fixed
|
|
afterwards; the target is 2. **When a ticket repeats an earlier sweep, re-run the earlier
|
|
sweep's exemption check against the new scope.** An exemption is a property of the
|
|
content, not of the directory that happened to hold it first.
|
|
|
|
- RD-20 required `gen:behaviour-spec` in its own Steps list, then asserted
|
|
`git grep -l "toWizardErrors" -- apps libs | wc -l` would be **5**: the helper, its spec
|
|
and the three call sites. The honest answer is 6. The generator publishes every `describe`
|
|
title, so naming the spec after the function it tests — which is house style — puts the
|
|
name into `behaviour-spec.mdx` as well. The agent correctly refused to rename the describe
|
|
block to satisfy the number. **A name that appears in a spec title also appears in the
|
|
generated document.** Count that file, or use `>=` as RD-17 did.
|
|
|
|
Four habits that prevent all five:
|
|
|
|
- **Use `git grep`, not `grep -r`.** It searches tracked files only, so untracked and
|
|
gitignored paths never pollute the result. Measured on this repo: `grep -r` finds 132
|
|
hits under `.claude/`, `git grep` finds 0. This matters most for RD-17, RD-18 and RD-19,
|
|
which are repo-wide sweeps.
|
|
- **Anchor on a declaration** (`^ onRetry\(\)`), not on a name that may legitimately
|
|
appear elsewhere.
|
|
- **`git grep -c` counts matching LINES, not occurrences.** RD-14 asserted
|
|
`git grep -c "'Idle'\|'Saving'\|'Saved'\|'Error'"` would be `>= 4`, but all four tags
|
|
live on one line of a single-line type declaration, so the honest answer is `1`. The
|
|
agent correctly refused to reformat the type across four lines to satisfy the number.
|
|
When you want occurrences, use `grep -o … | wc -l`; when a line count is what you mean,
|
|
say so. **A symbol you import and then use is two lines, never one.** RD-23 repeated the
|
|
mistake in the other direction, asserting `git grep -c "createUploadController"` would be
|
|
1 in the file that both imports and calls it. The only way to reach 1 is an import alias
|
|
that exists solely to satisfy the check.
|
|
- **Scope every acceptance command to the ticket's Files list, never to a parent
|
|
directory.** This is the habit most often broken, including by the supervisor in RD-12:
|
|
the check `git grep "ActionState" -- apps/ssp/src/app/brief` cannot pass, because
|
|
`org-template.store.ts` lives in that directory and is deliberately out of scope until
|
|
RD-13. Name the files. If a command reaches a file the ticket says not to touch, one of
|
|
the two is wrong.
|
|
- **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 — on the same line in one place — and the count held.
|
|
|
|
## Troubleshooting
|
|
|
|
**`dotnet test` fails with `SQLite Error 1: 'no such table: <X>'`.** Stale, gitignored
|
|
`bigregister.db` artifacts from an old build. Found during RD-11, where a 0-byte file dated
|
|
months earlier failed 6 backend tests on an otherwise clean tree. Delete all three and re-run:
|
|
|
|
```bash
|
|
rm -f backend/bigregister.db backend/src/BigRegister.Api/bigregister.db \
|
|
backend/tests/BigRegister.Tests/bin/Debug/net10.0/bigregister.db
|
|
```
|
|
|
|
These are build artifacts, not fixtures — removing them is always safe.
|
|
|
|
A second cause reaches the same error: **two agents running `dotnet test` at the same time in
|
|
one checkout.** They share those files, so one run truncates the other's database. RD-17 hit
|
|
this. The fix is the same three deletions, but the cause is concurrency — see the next entry.
|
|
|
|
**`dotnet test` reports one failure in `OpenZaakIntegrationTests`.** That test carries
|
|
`[Trait("Category", "Integration")]` and needs the live OpenZaak harness. Both `ci.yml` and
|
|
`scripts/ci-local.sh` filter it out. A bare `dotnet test` does not. Run `npm run ci` — a bare
|
|
`dotnet test` is the wrong command, not a red build.
|
|
|
|
**An unexplained dirty tree may be a running agent, not an interrupted session.** `/clear`
|
|
ends the supervisor's context. It does **not** stop the `developer` agent that session
|
|
spawned. That agent keeps writing to the same branch. In RD-17 a fresh supervisor read the
|
|
half-finished tree as an interrupted session, wrote a "continue from here" ticket, and
|
|
dispatched a second agent onto the work the first was still doing. Both agents then ran the
|
|
backend tests together, which produced the stale-database failure above.
|
|
|
|
Nothing was lost, because the second agent committed first and the first agent checked before
|
|
committing rather than committing blindly. That was luck. Before you dispatch, confirm no
|
|
agent is live:
|
|
|
|
```bash
|
|
git status --short # modified files nobody in THIS session touched = suspect
|
|
git log --oneline -1 # did an agent already commit the ticket?
|
|
```
|
|
|
|
If the tree is dirty and this session did not dirty it, find the agent before you write a
|
|
ticket around the evidence it leaves.
|
|
|
|
**The mirror-image mistake, made during RD-24: the supervisor became the second writer.** A
|
|
task notification fires every time an agent stops with **no live children — including a pause
|
|
mid-task**. RD-24's agent paused, said it was waiting on a check, and notified. The supervisor
|
|
read that as an abandoned task, took over the working tree, ran the gate and edited the ticket
|
|
`Status:` — while the agent was still running. The agent then resumed, correctly detected a
|
|
second process writing its tree, and refused to commit.
|
|
|
|
Nothing was lost, because the agent stopped instead of committing. Two rules follow:
|
|
|
|
- **A notification is not proof the work is finished.** A finished agent hands back a report
|
|
with acceptance numbers. "I am waiting for X" is a pause.
|
|
- **Before taking over an agent's tree, confirm the agent is gone**: `ps aux | grep "npm run"`,
|
|
and check whether its own result has arrived. Taking over is a real option — RD-24 was
|
|
finished that way — but it must be a decision, not an assumption.
|