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>
This commit is contained in:
eho
2026-09-08 23:16:26 +02:00
co-authored by Claude Opus 5
parent 241fe1b57b
commit 682db41344
3 changed files with 122 additions and 16 deletions
@@ -0,0 +1,106 @@
# RD-32 — `fp-tea-atomic-design.md` cites the pre-monorepo tree
Status: done
Phase: 5 — fix the docs that describe this flow
## Why
`docs/reference/fp-tea-atomic-design.md` is the learning guide. CLAUDE.md names it as one of
the three documents that hold the _why_. It still addresses the tree from before the monorepo
(WP-67): every path starts `src/app/`, which no directory has done since that work landed.
A reader learning the codebase from this guide cannot open a single file it names. The prose is
correct; only the addresses are wrong.
## Read first
- `docs/project/readable-codebase/PLAN.md`, phase 5 item 3 (line 833).
- `docs/reference/fp-tea-atomic-design.md` — the whole file.
## Decisions (pre-made, do not relitigate)
1. **Two mapping rules cover all 11 paths.** A context path becomes
`apps/ssp/src/app/<context>/…`. A `shared` path becomes `libs/shared/src/…` — note that
`app/shared/` collapses to `libs/shared/src/`, it does not become `libs/shared/src/app/`.
2. **Follow the file's own precedent.** Line 338 already reads
`libs/shared/src/application/store.ts`. Line 223 names the same file the old way. Make 223
match 338, not the reverse.
3. **Fix the broken anchor at line 435.** It reads
`#1-the-big-picture-three-contexts-four-layers`. The heading is now "1. The big picture: two
apps, cross-app libraries, "contexts", "layers"", so the anchor is
`#1-the-big-picture-two-apps-cross-app-libraries-contexts-layers`.
4. **Correct line 513's "Where" guidance.** It says `shared/ui/`. After RD-27 a shared block
lands in `libs/shared/src/ui/<atoms|molecules|organisms>/`. Name the layer folder; the whole
point of RD-27 is that the folder is the layer.
5. **Change addresses only.** Do not rewrite the teaching. This guide's prose survived the
refactor because it teaches ideas, not file layout.
## The facts, measured against the current tree
All 11 occurrences of `src/app/`, and where each file lives now:
| Line | Cited | Actual |
| -------- | --------------------------------------------------------- | ------------------------------------------------------------------ |
| 72 | `src/app/registratie/domain/value-objects/uren.ts` | `apps/ssp/src/app/registratie/domain/value-objects/uren.ts` |
| 98, 255 | `src/app/herregistratie/domain/herregistratie.machine.ts` | `apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts` |
| 223 | `src/app/shared/application/store.ts` | `libs/shared/src/application/store.ts` |
| 299 | `src/app/shared/kernel/fp.ts` | `libs/shared/src/kernel/fp.ts` |
| 305 | `src/app/herregistratie/ui/herregistratie-wizard/…` | `apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/…` |
| 381 | `src/app/showcase/concepts.page.ts` | `apps/ssp/src/app/showcase/concepts.page.ts` |
| 388 | `src/app/shared/infrastructure/scenario.ts` | `libs/shared/src/infrastructure/scenario.ts` |
| 414, 523 | `src/app/registratie/ui/address-fields/…` | `apps/ssp/src/app/registratie/ui/address-fields/…` |
| 588 | `src/app/herregistratie/domain/intake.machine.ts` | `apps/ssp/src/app/herregistratie/domain/intake.machine.ts` |
Every target exists. This is a rename, not a hunt.
**One correction to `PLAN.md`.** It says one path points at a deleted file,
`submit-herregistratie.ts`. That reference is no longer in the document —
`grep -n submit-herregistratie docs/reference/fp-tea-atomic-design.md` returns nothing. Do not
go looking for it. Record the correction in the ticket; leave `PLAN.md` itself alone, because
it is a design record of what was true when it was written.
Line 523 sits inside a code comment, not prose. Update it anyway; a stale path in an example is
read as a real path.
## Files
- `docs/reference/fp-tea-atomic-design.md`.
## Steps
1. Rewrite the 11 paths per the table.
2. Fix the anchor at line 435.
3. Fix the "Where" guidance at line 513.
4. `grep -n "src/app/" docs/reference/fp-tea-atomic-design.md` — every remaining hit must start
`apps/ssp/src/app/`.
5. `npm run ci`.
## Acceptance criteria
- [x] No path in the file starts with a bare `src/app/`.
- [x] Every cited path resolves. Verify each with `ls`.
- [x] The §1 anchor matches the current heading.
- [x] `npm run ci` is green.
## Verification
1. `npm run ci` — the format check covers the file.
2. `grep -oE '\`[^\`]_src/[^\`]_\.ts\`' docs/reference/fp-tea-atomic-design.md | tr -d '\`' |
sort -u | xargs ls` — every path must resolve, with no error.
3. Open `ARCHITECTURE.md` and confirm the §1 heading text still produces the anchor you wrote.
The slug is the heading in lower case, with punctuation removed and spaces turned into
hyphens.
## Out of scope
- `ARCHITECTURE.md`. RD-31 owns it.
- CLAUDE.md and `atomic-design.mdx`. RD-33 owns them.
- The teaching content, the diagrams, and the code examples.
## Risks
1. **`app/shared/` is not `libs/shared/src/app/`.** The `app` segment disappears for the
library. Getting this wrong produces a path that looks right and resolves to nothing.
2. **The anchor is easy to half-fix.** GitHub drops the quotation marks around "contexts" and
"layers" and the colon, and turns each space into one hyphen. Verify by clicking, not by
reasoning.
+1 -1
View File
@@ -126,7 +126,7 @@ two. Note that RD-15 exists because 22 abandoned agent worktrees are still on di
| RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | done | | RD-29 | The 3 atomic-ladder rules in dependency-cruiser | 27 | | done |
| RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | done | | RD-30 | Archive the finished backlogs (16,300 lines) + an archive README | 01 | | done |
| RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | done | | RD-31 | `ARCHITECTURE.md` section 6a: symbols not lines, 2 dead paths, new names | 03, 08, 16 | | done |
| RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | todo | | RD-32 | `fp-tea-atomic-design.md`: 11 broken paths + the broken anchor | 27 | | done |
| RD-33 | CLAUDE.md + `atomic-design.mdx` + the `ui-component` skill | 03, 27, 29 | yes | 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-34 | _(optional)_ `NO_SUBORGS`/`NO_TABLES` become `RemoteData.Empty` | 11 | | todo |
| RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo | | RD-35 | _(optional, last, alone)_ upload `type:` discriminant to `tag:` | 27 | | todo |
+15 -15
View File
@@ -69,7 +69,7 @@ add a b = a + b
``` ```
In this app, the parsers and reducers are pure. For example In this app, the parsers and reducers are pure. For example
(`src/app/registratie/domain/value-objects/uren.ts`): (`apps/ssp/src/app/registratie/domain/value-objects/uren.ts`):
```ts ```ts
export function parseUren(raw: string): Result<string, Uren> { export function parseUren(raw: string): Result<string, Uren> {
@@ -95,7 +95,7 @@ copies the old fields and overrides one.
``` ```
This app's reducers always return a fresh object — e.g. This app's reducers always return a fresh object — e.g.
`src/app/herregistratie/domain/herregistratie.machine.ts`: `apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts`:
```ts ```ts
export function setField(s: WizardState, key: keyof Draft, value: string): WizardState { export function setField(s: WizardState, key: keyof Draft, value: string): WizardState {
@@ -220,7 +220,7 @@ store. Cross-page state that _must_ be shared lives in one root singleton
### 4a. The store — TEA's runtime in ~10 lines ### 4a. The store — TEA's runtime in ~10 lines
`src/app/shared/application/store.ts`: `libs/shared/src/application/store.ts`:
```ts ```ts
export interface Store<Model, Msg> { export interface Store<Model, Msg> {
@@ -252,7 +252,7 @@ to change it — it runs the pure `update` and `set`s the new value.
### 4b. Model + Msg + reduce ### 4b. Model + Msg + reduce
Mapping the four TEA pieces to real code, using the herregistratie wizard (the smallest Mapping the four TEA pieces to real code, using the herregistratie wizard (the smallest
machine) as the example — `src/app/herregistratie/domain/herregistratie.machine.ts`: machine) as the example — `apps/ssp/src/app/herregistratie/domain/herregistratie.machine.ts`:
- **Model** → `WizardState` (the discriminated union from §2d). - **Model** → `WizardState` (the discriminated union from §2d).
- **Msg** → `WizardMsg`, every event as one union: - **Msg** → `WizardMsg`, every event as one union:
@@ -296,13 +296,13 @@ export function reduce(s: WizardState, m: WizardMsg): WizardState {
} }
``` ```
`assertNever` (`src/app/shared/kernel/fp.ts`) makes the switch **exhaustive**: add a new `assertNever` (`libs/shared/src/kernel/fp.ts`) makes the switch **exhaustive**: add a new
`Msg` variant and forget to handle it, and the build fails. (`intake.machine.ts` and `Msg` variant and forget to handle it, and the build fails. (`intake.machine.ts` and
`registratie-wizard.machine.ts` have larger unions, same exact shape.) `registratie-wizard.machine.ts` have larger unions, same exact shape.)
### 4c. view → template + `computed()` + `dispatch` ### 4c. view → template + `computed()` + `dispatch`
The container component (`src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts`) The container component (`apps/ssp/src/app/herregistratie/ui/herregistratie-wizard/herregistratie-wizard.component.ts`)
creates the store and derives view values with `computed()`: creates the store and derives view values with `computed()`:
```ts ```ts
@@ -378,14 +378,14 @@ doesn't loop on its own write — see the BRP prefill and policy-threshold effec
Each wizard exposes `state` as a **read-only signal**, deliberately public so the Each wizard exposes `state` as a **read-only signal**, deliberately public so the
teaching page can highlight the live state. See it on the in-app showcase teaching page can highlight the live state. See it on the in-app showcase
(`src/app/showcase/concepts.page.ts`, route `/concepts`): section 4 lights up the (`apps/ssp/src/app/showcase/concepts.page.ts`, route `/concepts`): section 4 lights up the
current `WizardState` among `Editing → Submitting → Submitted/Failed` as you drive the current `WizardState` among `Editing → Submitting → Submitted/Failed` as you drive the
form, and section 5 shows the intake steps re-deriving as you type. form, and section 5 shows the intake steps re-deriving as you type.
> **Discrepancy with the PRD — open question.** The PRD refers to a dedicated "state > **Discrepancy with the PRD — open question.** The PRD refers to a dedicated "state
> debug view" / inspector. **No such feature exists** in the code today. What exists is > debug view" / inspector. **No such feature exists** in the code today. What exists is
> the `/concepts` showcase (live state highlight) and the `?scenario=slow|loading|empty|error` > the `/concepts` showcase (live state highlight) and the `?scenario=slow|loading|empty|error`
> interceptor (`src/app/shared/infrastructure/scenario.ts`) for exercising async states. > interceptor (`libs/shared/src/infrastructure/scenario.ts`) for exercising async states.
> A JSON state inspector _would be trivial here_ — single one-way state means you could > A JSON state inspector _would be trivial here_ — single one-way state means you could
> render `JSON.stringify(state())` in a panel and watch every transition — precisely > render `JSON.stringify(state())` in a panel and watch every transition — precisely
> because of everything in Part 6. Treat building one as a future task, not documented > because of everything in Part 6. Treat building one as a future task, not documented
@@ -411,7 +411,7 @@ In this codebase the form **atoms** (`text-input`, `radio-group`) are thin wrapp
the design system. They take config via `input()` and — because they implement the design system. They take config via `input()` and — because they implement
Angular's `ControlValueAccessor` — emit changes through `[ngModel]` / `(ngModelChange)`. Angular's `ControlValueAccessor` — emit changes through `[ngModel]` / `(ngModelChange)`.
The `form-field` **molecule** composes a label + projected control + error. The The `form-field` **molecule** composes a label + projected control + error. The
`address-fields` **organism** (`src/app/registratie/ui/address-fields/address-fields.component.ts`) `address-fields` **organism** (`apps/ssp/src/app/registratie/ui/address-fields/address-fields.component.ts`)
composes three `form-field`s and emits with `output()`: composes three `form-field`s and emits with `output()`:
```ts ```ts
@@ -432,7 +432,7 @@ Molecules compose atoms; organisms compose molecules — exactly like composing
functions, where the composite is still pure. `address-fields` is pure because the functions, where the composite is still pure. `address-fields` is pure because the
`form-field` and `text-input` it's built from are pure. Each atomic level only uses the `form-field` and `text-input` it's built from are pure. Each atomic level only uses the
level(s) below it (see the hierarchy diagram in level(s) below it (see the hierarchy diagram in
[`ARCHITECTURE.md` §1](./architecture/ARCHITECTURE.md#1-the-big-picture-three-contexts-four-layers)). [`ARCHITECTURE.md` §1](./architecture/ARCHITECTURE.md#1-the-big-picture-two-apps-cross-app-libraries-contexts-layers)).
### 5c. Pages / containers are the TEA runtime (the shell) ### 5c. Pages / containers are the TEA runtime (the shell)
@@ -510,9 +510,9 @@ Each recipe follows the existing pattern and naming, and ends with the same remi
**When:** you genuinely need a new building block (not a one-off; reuse must earn it — **When:** you genuinely need a new building block (not a one-off; reuse must earn it —
see [CLAUDE.md §2](../../CLAUDE.md)). see [CLAUDE.md §2](../../CLAUDE.md)).
**Where:** `shared/ui/` if generic; a context's `ui/` if domain-specific. Pick the level **Where:** `libs/shared/src/ui/<atoms|molecules|organisms>/` if generic; a context's `ui/`
by composition: composes nothing → **atom**; composes atoms → **molecule**; composes if domain-specific. Pick the level by composition: composes nothing → **atom**; composes
molecules into a domain block → **organism**. atoms → **molecule**; composes molecules into a domain block → **organism**.
**Steps:** build it **pure/presentational**`input()`s for data/config, `output()`s **Steps:** build it **pure/presentational**`input()`s for data/config, `output()`s
for events, `computed()` for derived display; **no inject, no state, no effects**. Theme for events, `computed()` for derived display; **no inject, no state, no effects**. Theme
@@ -520,7 +520,7 @@ only with design tokens (no hardcoded hex — CI checks via `npm run check:token
Add a co-located `*.stories.ts` titled `Layer/Name`. Add a co-located `*.stories.ts` titled `Layer/Name`.
```ts ```ts
// shape — see src/app/registratie/ui/address-fields/address-fields.component.ts // shape — see apps/ssp/src/app/registratie/ui/address-fields/address-fields.component.ts
export class AddressFieldsComponent { export class AddressFieldsComponent {
value = input.required<AdresValue>(); value = input.required<AdresValue>();
errors = input<AdresErrors>({}); errors = input<AdresErrors>({});
@@ -585,7 +585,7 @@ _This is the same loop, again — the rule is just another pure function._
**Steps:** compose AC. Model the step's state in the Model; **derive** the visible steps **Steps:** compose AC. Model the step's state in the Model; **derive** the visible steps
rather than storing "next" — copy `visibleSteps(answers)` from rather than storing "next" — copy `visibleSteps(answers)` from
`src/app/herregistratie/domain/intake.machine.ts`: `apps/ssp/src/app/herregistratie/domain/intake.machine.ts`:
```ts ```ts
export function visibleSteps(a: Answers): StepId[] { export function visibleSteps(a: Answers): StepId[] {