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>
This commit is contained in:
eho
2026-09-09 16:14:56 +02:00
co-authored by Claude Opus 5
parent 682db41344
commit d9aef9541f
6 changed files with 179 additions and 23 deletions
+28 -11
View File
@@ -81,10 +81,11 @@ session protocol is the worked example of this in practice.
`apps/<app>/src/app/<context>/<layer>/` for an app-local context; `libs/<lib>/src/<layer>/`
for a cross-app library (WP-67). Two apps today: `apps/ssp` (Zorgverlener self-service —
contexts `auth`, `registratie`, `herregistratie`, `brief` (letter-composition teaching
slice), `showcase` (teaching page, not a feature; **sanctioned** to read every context in
its own app — nothing imports it)) and `apps/behandelportal` (Behandelaar backoffice,
ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
contexts `auth`, `overzicht` (the portal home; composes `registratie`'s dashboard sections
plus its own cross-context nav sections), `registratie`, `herregistratie`, `brief`
(letter-composition teaching slice), `showcase` (teaching page, not a feature; **sanctioned**
to read every context in its own app — nothing imports it)) and `apps/behandelportal`
(Behandelaar backoffice, ADR-0002 — contexts `auth`, `behandeling`). Two cross-app libraries: `libs/shared` (the
design system + kernel + generated API client — no business logic) and `libs/beheer` (the
admin/stamdata context, identical for both apps today — WP-67 folded a silently-diverging
duplicate copy back into one). `auth` is deliberately **not** shared even though today it's
@@ -106,10 +107,11 @@ depend on `libs/beheer` either — it stays the base). `ui`/`layout` never impor
lint-enforced (per app, since each app is cruised against its own tsconfig — WP-67's
`.dependency-cruiser.base.js` + one thin `.dependency-cruiser.<app>.js` per app). An app
may not import the other app's source directly. Cross-context only
`herregistratie → registratie → libs/shared|beheer`, `auth → libs/shared|beheer`,
`brief → libs/shared|beheer` (ssp); `behandeling → libs/shared|beheer`, `auth →
libs/shared|beheer` (behandelportal). Imports use aliases as direction statements:
`@shared/* @beheer/* @auth/* @registratie/* @herregistratie/* @brief/*` (ssp) —
`overzicht → registratie → libs/shared|beheer`, `herregistratie → registratie →
libs/shared|beheer`, `auth → libs/shared|beheer`, `brief → libs/shared|beheer` (ssp);
`behandeling → libs/shared|beheer`, `auth → libs/shared|beheer` (behandelportal). Imports use
aliases as direction statements:
`@shared/* @beheer/* @auth/* @overzicht/* @registratie/* @herregistratie/* @brief/*` (ssp) —
`@shared/* @beheer/* @auth/* @behandeling/*` (behandelportal); each app's own
`tsconfig.json` declares its full map (the root `tsconfig.json` intentionally has no
`paths` — see its comment). `domain/` imports nothing from Angular.
@@ -129,6 +131,18 @@ the design system does the visuals. (Where CIBG lacks a class — e.g. `skeleton
`// CIBG-GAP EXTENSION:` marker; see ADR-0003. `alert` is **not** such a case: it wraps the
vendored `.feedback feedback-*` classes.)
**The step-component contract.** A wizard step follows the same rule as
`address-fields.component.ts`: values in, events out, no internal state. Three clauses:
1. **Inputs down.** A step reads its data only from `input()`s the container passes it.
2. **One narrow output up.** A step emits one specific event, not the container's whole
`dispatch`.
3. **`dispatch` is never passed down.** The container owns the Model and decides what a
step's event means; a step never calls `dispatch` itself.
Corollary: a step gets **no** story of its own. The wizard's own story already mounts every
step, because it seeds the machine.
### 3. State: make illegal states unrepresentable
Default reflex — **if you're about to add a second/third boolean to track state,
@@ -136,7 +150,7 @@ model a discriminated union instead.** Three tools, all in `libs/shared/src/appl
- **`RemoteData<E,T>`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`.
Combine sources with `map`/`map2`/`andThen` (Failure > Loading > Success).
Render it via the `<app-async>` molecule (`libs/shared/src/ui/async`) — one of four
Render it via the `<app-async>` molecule (`libs/shared/src/ui/molecules/async`) — one of four
templates, mutually exclusive by construction. Default loading spinner/skeleton
is delay-gated (~250ms) so fast connections don't flash.
- **Elm-style store** (`store.ts``createStore(initial, reduce)`) — all state in
@@ -242,7 +256,7 @@ organism doesn't get its own `Organisms/` bucket).
(`` $localize`:@@context.key:Tekst` ``). Source locale is `nl`; a second locale is a
translation file, not a code change (the seam). Shared/English components must **not**
hardcode Dutch — expose copy as `input()`s with localizable defaults; the domain caller
supplies the text (see `libs/shared/src/ui/async`). Format-validation messages in
supplies the text (see `libs/shared/src/ui/molecules/async`). Format-validation messages in
`domain/value-objects/` stay co-located but are still `$localize`-wrapped.
- **Forms = one idiom.** Any form with validation or submission uses a `*.machine.ts`
(Model/Msg/reduce) + value objects + a `submit-*` command returning `Result` — the
@@ -272,7 +286,10 @@ organism doesn't get its own `Organisms/` bucket).
- Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`,
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
- **Enforced, not just hoped-for:** `npm run lint` (`eslint.config.mjs`, scoped to
`{apps,libs}/**`) fails the build on `any`; `npm run dep:check`
`{apps,libs}/**`) fails the build on `any`; the same config's `max-lines` rule caps every
`{apps,libs}/**/*.{page,component,section,step}.ts` file at 250 lines
(`skipBlankLines: true`, `skipComments: true`). 250 is reachable, not a style-guide
default — the dashboard page lands at 42 lines. `npm run dep:check`
(`.dependency-cruiser.base.js` + one `.dependency-cruiser.<app>.js` per app, WP-67) fails
on illegal imports — `domain/` importing Angular, a context importing "upward" (the
`herregistratie → registratie → shared`, `auth → shared` direction), an app importing the