docs: reorganize into project/ + reference/, expand Storybook Foundations

Move working docs (backlog, prd, roadmap) under docs/project/ and durable
docs (architecture ADRs, guides, audits) under docs/reference/; add a
docs/README.md index. Update every path reference in code comments, CLAUDE.md,
READMEs, and the new-ssp skill. Expand the Storybook Foundations curriculum
(Overview, BDD, i18n; rename Layers→Domain-Driven Design) and reorder the sidebar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-20 20:33:32 +02:00
co-authored by Claude Opus 4.8
parent 55a0a2d166
commit d4e5a76873
40 changed files with 1699 additions and 1087 deletions
@@ -21,7 +21,7 @@ type Err = Error | undefined;
* The dashboard data now comes from ONE screen-shaped ("BFF-lite") call that
* returns registration + person + server-computed `decisions`. One request → one
* consistent snapshot, instead of stitching three independently loading/erroring
* resources together client-side. See docs/architecture/0001-bff-lite-decision-dtos.md.
* resources together client-side. See docs/reference/architecture/0001-bff-lite-decision-dtos.md.
*/
@Injectable({ providedIn: 'root' })
export class BigProfileStore {
@@ -3,7 +3,7 @@
*
* In production this is GENERATED from the OpenAPI/TypeSpec spec and served by our
* own backend, which talks to the BRP behind an adapter. The frontend never sees
* the BRP's own wire format. See docs/architecture/0001-bff-lite-decision-dtos.md.
* the BRP's own wire format. See docs/reference/architecture/0001-bff-lite-decision-dtos.md.
*
* "Geen adres bekend" is a first-class outcome (`gevonden: false`), not an error —
* the wizard falls back to manual entry (PRD §7). Slice 1 ships only the happy
@@ -10,7 +10,7 @@
* In production these types are GENERATED from the OpenAPI/TypeSpec spec (one
* source of truth for both sides), and the `decisions` block is computed BY THE
* BACKEND — never recomputed on the client. The frontend renders decisions; it
* does not own the rules. See docs/architecture/0001-bff-lite-decision-dtos.md.
* does not own the rules. See docs/reference/architecture/0001-bff-lite-decision-dtos.md.
*
* One screen-shaped call replaces the previous three (BIG-register + BRP + …),
* so the page always sees one consistent snapshot instead of three independently
@@ -25,7 +25,7 @@ const meta: Meta<AanvraagBlockComponent> = {
parameters: {
// Structural: app-aanvraag-block's host sits between the keuzelijst <ul> and its <li>
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/backlog/WP-11-markup-fidelity.md.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
};
@@ -15,7 +15,7 @@ const meta: Meta<ChoiceLinkComponent> = {
parameters: {
// Structural: app-choice-link's host sits between the <ul> and its <li> — axe's
// list/listitem rule requires them adjacent regardless of `display:contents`.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/backlog/WP-11-markup-fidelity.md.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
};
@@ -21,7 +21,7 @@ const meta: Meta<ChoiceListComponent> = {
}),
parameters: {
// Structural: app-choice-link's host sits between the <ul> and its <li> —
// fixed by the WP-11 markup rework. See docs/backlog/WP-11-markup-fidelity.md.
// fixed by the WP-11 markup rework. See docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
};
@@ -14,7 +14,7 @@ const meta: Meta<TaskListComponent> = {
parameters: {
// Structural: app-choice-link's host sits between the keuzelijst <ul> and its <li>
// — axe's list/listitem rule needs them adjacent regardless of `display:contents`.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/backlog/WP-11-markup-fidelity.md.
// WP-11 (CIBG markup fidelity) reworks this markup; see docs/project/backlog/WP-11-markup-fidelity.md.
a11y: { disable: true },
},
};
+2 -2
View File
@@ -30,7 +30,7 @@ what the ones below/above it can't.
`-desc`/`-error` in the right order; `alert.stories.ts` asserts `role="alert"` for
errors vs `role="status"` for info/ok/warning. These run as part of the same
`test-storybook:ci` gate as the axe checks, so a regression fails CI, not just a panel.
4. **Manual WCAG checklist** (`docs/wcag-checklist.md`) — what none of the above can see:
4. **Manual WCAG checklist** (`docs/reference/wcag-checklist.md`) — what none of the above can see:
tab order across a whole page, focus traps, 200%-zoom reflow, and how a real screen
reader narrates a flow. A living per-page checklist, not a one-time audit — it already
caught a real bug (a dashboard alert overflowing at 320px) that no automated layer here
@@ -65,5 +65,5 @@ the same way (`withInMemoryScrolling`), both wired once in `app.config.ts` — n
fails it on a real axe violation. Both can be locally disabled — the lint rule via a
normal ESLint disable comment, axe via `parameters: { a11y: { disable: true } }` — but
only with a comment naming _why_ and a cross-reference to the WP expected to remove the
skip (see `docs/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
skip (see `docs/project/backlog/WP-13-cibg-gap-register.md`'s marker convention, reused here).
Grep `a11y: { disable: true }` in `*.stories.ts` for the current list.
+1 -1
View File
@@ -106,7 +106,7 @@ Periodically we audit for near-duplicate blocks. Some collapse into one; a few *
similar but earn their separation. This table records the "don't merge these" verdicts so
the next person doesn't spend an afternoon re-deciding. (Deliberate CIBG-specific deviations
live in [CIBG gaps](?path=/docs/foundations-cibg-gaps--docs); the FE⇄DS "same shape, different
context" cases in [Layers](?path=/docs/foundations-layers--docs).)
context" cases in [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs).)
| Pair | Why kept separate |
| --- | --- |
+75
View File
@@ -0,0 +1,75 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/BDD" />
# Behaviour-driven tests
Tests here read as **specifications of behaviour**, not checks of implementation. A test
says what the system *does* — in the domain's own words — so a failing test names a broken
behaviour, and the suite doubles as living documentation. This is the BDD half of the
[Testing strategy](?path=/docs/foundations-testing-strategy--docs) (which owns *what to
test, by layer*); BDD owns *how each test is phrased and scoped*.
## Three rules
### 1. `describe` = the subject, `it` = one observable behaviour
The `describe()` block names the unit under test; each `it()` states a single behaviour in
**declarative present tense** — the implicit subject is "it". No `should`, no
Given/When/Then ceremony: present-tense declaration already reads as a spec.
```ts
describe('parsePostcode', () => {
it('normalises to "1234 AB" (uppercase, single space, trimmed)', () => { … });
it('rejects malformed input', () => { … });
});
```
Read top-to-bottom it *is* the spec: "parsePostcode — normalises to 1234 AB; rejects
malformed input."
### 2. One behaviour per test
A test asserts **one behaviour**, not one `expect()`. Several assertions that pin down the
*same* behaviour belong together; assertions about *different* behaviours belong apart.
| Keep together (one behaviour) | Split apart (separate behaviours) |
| --- | --- |
| A `Result`'s `.ok` then its `.value` | The `ok` branch **and** the `err` branch of a transition |
| A whole-object `toEqual` | An invalid-input case **and** a valid-input case |
| A loop asserting one rule over many inputs | Two independent state transitions |
| A truth-table (`draft` → true, `approver` → false) of one rule | An authorization check **and** a rendering check |
A title that needs `/`, `;`, "then" or "and" to join two behaviours is the smell — split it,
and each half gets its own present-tense name.
### 3. Speak the ubiquitous language (the DDD tie-in)
Test names use the **domain vocabulary**, not technical jargon — the same words as the
[bounded contexts](?path=/docs/foundations-domain-driven-design--docs): a *behandelaar*
drafts, a *beoordelaar* approves, a *herregistratie* is *ingediend*. The test name is
readable by someone who knows the domain but not the code.
```ts
it('drafter cannot approve or reject even when submitted', …);
it('confirmed dutch proficiency requires taalvaardigheid proof', …);
```
## How it fits TDD & DDD
- **TDD** — the loop is red → green → refactor: write the behaviour as a failing `it`, make
it pass, then clean up. Because tests describe behaviour (not internals), a refactor that
preserves behaviour keeps them green. Pure domain logic is tested directly — no `TestBed`
(see [Testing strategy](?path=/docs/foundations-testing-strategy--docs)).
- **DDD** — behaviour is expressed in the ubiquitous language, so the spec and the code
share one vocabulary. Domain rules (reducers, value-object parsers, policies) are the
richest specs; the wire boundary is tested as "rejects malformed input", the UI as
Storybook stories.
## Where to look
Canonical behaviour specs in the repo: `registratie/domain/value-objects/postcode.spec.ts`
(parser behaviour), `registratie/domain/registratie-wizard.machine.spec.ts` (one transition
per test), and backend `AuthzTests.cs` (rule truth-tables). The
[Testing strategy](?path=/docs/foundations-testing-strategy--docs) page maps which layer
gets which kind of test.
+1 -1
View File
@@ -7,7 +7,7 @@ import * as AsyncStories from '../app/shared/ui/async/async.stories';
The components in this library are the _view_. Behind them, three small functional tools do
the heavy lifting — all so that **illegal states can't be represented**. This page is the
Storybook front door; the full narrative lives in `docs/fp-tea-atomic-design.md`, and a
Storybook front door; the full narrative lives in `docs/reference/fp-tea-atomic-design.md`, and a
side-by-side "before/after" runs at the app's **`/concepts`** route.
## 1. `RemoteData<E,T>` — async has four states, not a boolean soup
+7 -5
View File
@@ -1,12 +1,14 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Layers & Contexts" />
<Meta title="Foundations/Domain-Driven Design" />
# Layers & contexts
# Domain-driven design: bounded contexts & layers
This sidebar is laid out to **be** the architecture, not just document it: **Foundations**
(this curriculum) → **Design System** (reusable, domain-free) → **Domein** (the six DDD
contexts). If a component lives under a context's `ui/`, it's in Domein; everything else
This project is **domain-driven**: the code is organised first by **bounded context**
(a business capability with its own language) and then by **layer** inside each context,
with dependencies pointing inward. The Storybook sidebar is laid out to **be** that
architecture, not just document it: **Foundations** (this curriculum) → **Design System**
(reusable, domain-free) → **Domein** (the six DDD contexts). If a component lives under a context's `ui/`, it's in Domein; everything else
in `shared/ui`/`shared/layout` is Design System. See [Atomic design](?path=/docs/foundations-atomic-design--docs)
for the Atoms → Molecules → Organisms → Templates ladder inside Design System.
+35
View File
@@ -0,0 +1,35 @@
import { Meta } from '@storybook/addon-docs/blocks';
<Meta title="Foundations/Overview" />
# Foundations
The **why** behind this codebase, as a short curriculum. Each page is a condensed,
cross-linked take on one idea; the long-form source lives in `docs/reference/`
(see the repo's `docs/README.md`). Read them in roughly this order.
## Architecture & domain
- [Domain-driven design](?path=/docs/foundations-domain-driven-design--docs) — bounded
contexts + layers, dependencies pointing inward (the folder structure *is* the architecture).
- [Atomic design](?path=/docs/foundations-atomic-design--docs) — Atoms → Molecules →
Organisms → Templates; a new page is composition, not new building blocks.
## Functional core
- [FP in the UI](?path=/docs/foundations-fp-in-the-ui--docs) — the three functional tools behind the view.
- [State machines (TEA)](?path=/docs/foundations-state-machines-tea--docs) — every form/wizard as Model → Msg → pure `reduce`.
- [RemoteData & Async](?path=/docs/foundations-remotedata-async--docs) — the four async states as one value.
- [Parse, don't validate](?path=/docs/foundations-parse-dont-validate--docs) — narrow untrusted `unknown` at the boundary into domain types.
## Design system
- [Design tokens](?path=/docs/foundations-design-tokens--docs) — semantic `--rhc-*` tokens; no hand-written colours.
- [CIBG gap register](?path=/docs/foundations-cibg-gap-register--docs) — where we diverge from the CIBG Huisstijl (ADR-0003).
## Quality & process
- [Accessibility](?path=/docs/foundations-accessibility--docs) — four layered a11y tools, each catching a different bug class.
- [Testing strategy](?path=/docs/foundations-testing-strategy--docs) — what to test, by layer grain.
- [BDD](?path=/docs/foundations-bdd--docs) — how each test is phrased and scoped: one behaviour, in the domain's language.
- [Internationalization](?path=/docs/foundations-internationalization--docs) — `$localize` for every user-visible string; the locale seam.
+3 -1
View File
@@ -6,7 +6,9 @@ import { Meta } from '@storybook/addon-docs/blocks';
Tests follow the same grain as the architecture: **push the logic down to where it's pure,
test it there directly, and keep the layers above thin.** No single tool covers everything,
so each layer gets the cheapest test that catches its class of bug.
so each layer gets the cheapest test that catches its class of bug. This page owns *what to
test, by layer*; how each test is **phrased and scoped** — one behaviour, in the domain's
language — is [BDD](?path=/docs/foundations-bdd--docs).
## What gets tested where