feat(boundaries): WP-04 — ui ↛ infrastructure + showcase sanction

Move the two wizard lookups behind application-layer facades so ui/ no longer
injects infrastructure adapters directly:
- RegistratieLookupStore (BRP address + DUO diplomas): owns the resources,
  runs the trust-boundary parse, exposes adresStatus/prefillAdres/duoLookup.
- IntakePolicyStore (scholing threshold): owns the policy resource, exposes
  the derived threshold.

Add the lint rule ui/ + layout/ ↛ **/infrastructure/** (@typescript-eslint
variant so it composes with the base direction rules; stories/specs exempted
as test scaffolding). Add the documented showcase sanction (may read every
context). Fix the docs' inventory: 6 contexts / 5 layers, +brief, +contracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 21:01:42 +02:00
parent 3a5c8f157a
commit 035e785c95
10 changed files with 513 additions and 227 deletions

View File

@@ -16,7 +16,7 @@ re-registration — "herregistratie").
---
## 1. The big picture: three "contexts", four "layers"
## 1. The big picture: six "contexts", five "layers"
The code is split first by **business area** (a "bounded context" in DDD terms),
then inside each area by **layer**.
@@ -27,9 +27,14 @@ src/app/
auth/ logging in / the current session
registratie/ the user's BIG registration + personal data
herregistratie/ the re-registration application flow
showcase/ a teaching page; not a real feature
brief/ letter-composition teaching slice
showcase/ a teaching page; not a real feature (may read every context)
```
`showcase/` is a **sanctioned exception** to the direction rules: its whole point is
showing multiple contexts side by side, so it may import any context. Nothing imports
`showcase`. (Enforced in `eslint.config.mjs`; same precedent as the `debug-state` panel.)
### The atomic-design hierarchy, visualised
The UI is built bottom-up: tiny **atoms** combine into **molecules**, which combine
@@ -53,7 +58,7 @@ organism** (`intake-wizard`) — everything else (`form-field`, `text-input`, `b
`alert`, `spinner`, the page shell) was reused unchanged. That is the payoff of the
hierarchy.
Inside a context you'll see the same four folders. They answer four different
Inside a context you'll see the same five folders. They answer five different
questions:
| Layer | Answers… | May import Angular? | Example here |
@@ -61,14 +66,18 @@ questions:
| `domain/` | What are the business rules and data? | **No** (pure TS) | `registration.ts`, `registration.policy.ts` |
| `application/` | How do we coordinate a task / state? | Yes (signals) | `big-profile.store.ts` |
| `infrastructure/` | Where does data come from? | Yes (HTTP) | `big-register.adapter.ts`, `brp.adapter.ts` |
| `contracts/` | What's the FE⇄BE wire shape? | **No** (pure DTOs) | `dashboard-view.dto.ts` |
| `ui/` | How does it look? | Yes (components) | `dashboard.page.ts` |
**The one rule that keeps it sane: dependencies only point _inward_.** UI may use
application, application may use domain, everyone may use `shared`. Never the
other way around. The `domain/` layer imports nothing from Angular, so the
business rules are plain functions you can read and test in isolation.
other way around. In particular **`ui/` and `layout/` never import `infrastructure/`
directly** — they reach data through an application store or command (lint-enforced).
The `domain/` layer imports nothing from Angular, so the business rules are plain
functions you can read and test in isolation.
Allowed direction: `herregistratie → registratie → shared`, `auth → shared`.
Allowed direction: `herregistratie → registratie → shared`, `auth → shared`,
`brief → shared` (`showcase` may read every context; see above).
### Why the `shared/` kernel is split too
@@ -79,7 +88,7 @@ Allowed direction: `herregistratie → registratie → shared`, `auth → shared
- `shared/infrastructure/` — the demo HTTP interceptor.
Imports use path aliases so they read as direction statements:
`@shared/*`, `@auth/*`, `@registratie/*`, `@herregistratie/*`.
`@shared/*`, `@auth/*`, `@registratie/*`, `@herregistratie/*`, `@brief/*`.
---

View File

@@ -43,7 +43,7 @@ for its existing violations, so every WP ends green.
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | todo |
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | todo |
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | todo |
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | todo |

View File

@@ -1,6 +1,6 @@
# WP-04 — Boundaries II: `ui ↛ infrastructure` + showcase sanction
Status: todo
Status: done (pending commit)
Phase: 0 — enforcement & gates
## Why
@@ -54,11 +54,13 @@ Phase 0 — a pure move of wiring, no behavior change.
## Acceptance criteria
- [ ] Rule active; lint green; no disables beyond the documented showcase + debug-state
exemptions.
- [ ] No `**/ui/**` file imports from `**/infrastructure/**`.
- [ ] Both wizards behave unchanged (specs pass; manual smoke).
- [ ] CLAUDE.md and ARCHITECTURE.md list 6 contexts / 5 layers.
- [x] Rule active; lint green; no disables beyond the documented showcase + debug-state
exemptions. (Probed: a ui→infra import errors.)
- [x] No `**/ui/**` file imports from `**/infrastructure/**` (production; stories/specs
exempted — test scaffolding wires the real client).
- [x] Both wizards behave unchanged (178 specs pass; storybook a11y suite mounts both
wizards green — behaviour is a pure wiring move behind root facades).
- [x] CLAUDE.md and ARCHITECTURE.md list 6 contexts / 5 layers.
## Verification