Add FP + Elm Architecture + atomic design learning guide

A progressive teaching guide (docs/fp-tea-atomic-design.md): FP fundamentals,
The Elm Architecture, and atomic design, taught Elm-then-this-app with the real
store/machine/value-object code, plus four recipes and a glossary. It owns the
teaching arc and cross-references ARCHITECTURE.md/ADR-0001 rather than duplicating
them. Documents reality where the PRD diverged (no state-debug-view feature; per-
wizard stores; reduce vs update naming) and flags the absent debug view as an open
question. Adds pointer links from ARCHITECTURE.md and CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 17:40:38 +02:00
parent 770d454a32
commit e5a3030dca
4 changed files with 2062 additions and 1162 deletions

View File

@@ -1,8 +1,10 @@
# CLAUDE.md
Agent guide for this repo. The *why* lives in `docs/ARCHITECTURE.md` and
`docs/architecture/0001-bff-lite-decision-dtos.md`; this file is the *rules*.
When a decision below and those docs disagree, the docs win — update this file.
Agent guide for this repo. The _why_ lives in `docs/ARCHITECTURE.md`,
`docs/architecture/0001-bff-lite-decision-dtos.md`, and the learning guide
`docs/fp-tea-atomic-design.md` (FP + The Elm Architecture + atomic design); this
file is the _rules_. When a decision below and those docs disagree, the docs win —
update this file.
POC of a Dutch BIG-register self-service portal (healthcare professionals log in,
view their registration, apply for re-registration). Angular 22, standalone,
@@ -24,16 +26,17 @@ advisories are pinned via `package.json` `overrides`; the shipped bundle audits
## The decisions (non-negotiable working agreements)
### 1. DDD: contexts then layers, dependencies point inward
`src/app/<context>/<layer>/`. Contexts: `shared`, `auth`, `registratie`,
`herregistratie`, `showcase` (teaching page, not a feature).
| Layer | Job | Angular allowed? |
|---|---|---|
| `domain/` | business rules + data types | **No — pure TS.** Has `.spec.ts` |
| `application/` | coordinate state/tasks (stores, commands) | yes (signals) |
| `infrastructure/` | where data comes from (HTTP adapters) | yes (HTTP) |
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
| `ui/` | how it looks (components, pages) | yes |
| Layer | Job | Angular allowed? |
| ----------------- | ----------------------------------------- | -------------------------------- |
| `domain/` | business rules + data types | **No — pure TS.** Has `.spec.ts` |
| `application/` | coordinate state/tasks (stores, commands) | yes (signals) |
| `infrastructure/` | where data comes from (HTTP adapters) | yes (HTTP) |
| `contracts/` | wire DTOs (the FE⇄BE seam) | no |
| `ui/` | how it looks (components, pages) | yes |
**Dependencies only point inward**: `ui → application → domain`; everyone may use
`shared`; never the reverse. Cross-context only `herregistratie → registratie → shared`,
@@ -41,6 +44,7 @@ advisories are pinned via `package.json` `overrides`; the shipped bundle audits
@registratie/* @herregistratie/*`. `domain/` imports nothing from Angular.
### 2. Atomic design: folder = layer
`shared/ui` atoms → molecules → organisms; `shared/layout` templates (`shell`,
`page-shell`); context `ui/` pages. Each level only uses levels below. A new page
should be **composition of existing blocks** — adding building blocks is the
@@ -48,6 +52,7 @@ exception, not the default. Atoms are thin wrappers over Utrecht/RHC CSS classes
we own only a small typed `input()` API, the design system does the visuals.
### 3. State: make illegal states unrepresentable
Default reflex — **if you're about to add a second/third boolean to track state,
model a discriminated union instead.** Three tools, all in `shared/application`:
@@ -67,7 +72,7 @@ model a discriminated union instead.** Three tools, all in `shared/application`:
**Derive, don't store** what you can compute — e.g. the wizard's visible steps are
`visibleSteps(answers)`, not a stored field (`intake.machine.ts`).
**Side effects stay out of the reducer.** A *command* (`application/submit-*.ts`)
**Side effects stay out of the reducer.** A _command_ (`application/submit-*.ts`)
does the HTTP, then dispatches a message describing the outcome. Reducer = "what the
new state is"; command = "go do it, then say what happened."
@@ -77,11 +82,12 @@ NgRx, no extra lib. Optimistic update pattern: `begin*` (flip pending) →
`confirm*` (clear + `resource.reload()`) / `rollback*` (undo).
### 4. BFF-lite + decision DTOs (ADR-0001)
`infrastructure/` is the **only** layer that touches the network — the
anti-corruption boundary. Each screen gets one screen-shaped endpoint returning a
decision-enriched DTO; **the FE renders decisions, it does not recompute business
rules.** Per rule, pick: *decision flag* (server computes the boolean — e.g.
herregistratie eligibility) or *config value* (server sends threshold, FE applies
rules.** Per rule, pick: _decision flag_ (server computes the boolean — e.g.
herregistratie eligibility) or _config value_ (server sends threshold, FE applies
for instant feedback, server re-validates as authority — e.g. scholing threshold).
FE keeps only **format** validation, never as authority.
@@ -92,6 +98,7 @@ rules stay in `domain/*.policy.ts` as reference impl + unit test, marked server-
but the FE doesn't call them.
### 5. Testing
Vitest. Co-locate `*.spec.ts` next to the unit. **Domain and pure logic must have a
spec** (reducers, combinators, `visibleSteps`, parsers, boundary `parse*` adapters).
Test the pure function directly — no Angular TestBed for domain. UI is exercised via
@@ -99,13 +106,14 @@ Storybook stories (`*.stories.ts` co-located, titled `Layer/Name`, a11y addon on
not heavy component tests.
## Conventions
- Standalone components only; no NgModules. Signal inputs (`input()`), `inject()` over
constructor DI (constructor only for `effect()`/template-ref injection).
- Angular-native control flow `@if/@for`; native `httpResource` for fetching;
`withViewTransitions()` for page transitions (header/footer have stable
`view-transition-name`, excluded from the fade).
- Routes: lazy `loadComponent`, persistent `ShellComponent` parent, `canActivate:
[authGuard]` on protected routes (`app.routes.ts`).
[authGuard]` on protected routes (`app.routes.ts`).
- Theming is one import — `src/styles.scss` pulls the RHC palette; no hand-written theme.
- Scenario toggle: `?scenario=slow|loading|empty|error` on data pages
(`scenario.interceptor.ts`) to see every async state.
@@ -113,11 +121,13 @@ not heavy component tests.
`noPropertyAccessFromIndexSignature`, `noFallthroughCasesInSwitch`, `isolatedModules`.
## Adding a feature (recipe)
Domain first (types + pure rules + spec, no Angular) → infrastructure (adapter:
`httpResource` or command returning `Result`) → application (store if shared state;
union + pure reduce) → UI last (compose `shared/ui` atoms, wrap async in `<app-async>`,
dispatch messages). Worked example: the intake wizard (`herregistratie/`).
## Out of scope (POC, don't build unprompted)
Real auth/DigiD, real backend, i18n, NgRx, licensed Rijkshuisstijl font/logo,
runtime DTO validation on every endpoint, multi-tab session sync, OpenAPI codegen.