# CLAUDE.md 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, signals. Auth is faked; **data and business rules are served by a minimal ASP.NET Core backend** (`backend/`, see its README — in-memory seeded, no DB) and consumed through an NSwag-generated typed client. The FE renders the backend's decisions. ## Commands ```bash npm start # ng serve (proxies /api → backend) → http://localhost:4200 npm test # vitest npm run build # ng build (must stay green) npm run storybook # component library by atomic layer npm run gen:api # regenerate the typed client from the backend OpenAPI doc docker compose up # run FE + backend together (Swagger at :5000/swagger) cd backend && dotnet test # backend rule + endpoint tests ``` `.npmrc` sets `legacy-peer-deps=true` (Storybook's peer range lags Angular 22). Do not run `npm audit fix --force` — it downgrades Angular 22→21. Dev-only advisories are pinned via `package.json` `overrides`; the shipped bundle audits clean. ## The decisions (non-negotiable working agreements) ### 1. DDD: contexts then layers, dependencies point inward `src/app///`. 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 | **Dependencies only point inward**: `ui → application → domain`; everyone may use `shared`; never the reverse. Cross-context only `herregistratie → registratie → shared`, `auth → shared`. Imports use aliases as direction statements: `@shared/* @auth/* @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 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`: - **`RemoteData`** (`remote-data.ts`) — `Loading | Empty | Failure{error} | Success{value}`. Combine sources with `map`/`map2`/`map3`/`andThen` (Failure > Loading > Success). Render it via the `` molecule (`shared/ui/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 one Model; change only by `dispatch(msg)` → **pure** `reduce(model, msg)`. Models are tagged unions (see `herregistratie.machine.ts`, `intake.machine.ts`). Templates send messages, never mutate. - **`Result` + value objects** ("parse, don't validate") — raw input becomes a branded type only via a parser returning `Result` (`registratie/domain/value-objects/`: `Postcode`, `Uren`, `BigNummer`). Once you hold the type, never re-check it. **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`) does the HTTP, then dispatches a message describing the outcome. Reducer = "what the new state is"; command = "go do it, then say what happened." **Shared cross-page state = one root singleton.** Stores are `providedIn: 'root'` (`BigProfileStore`, `SessionStore`). That single instance is the shared state — no 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 for instant feedback, server re-validates as authority — e.g. scholing threshold). FE keeps only **format** validation, never as authority. DTO lives in `contracts/`; a hand-written `parse*`/`toDomain` in `infrastructure/` validates the untrusted shape and maps DTO → domain. Wiring a real .NET backend touches only `infrastructure/` + `contracts/` (see ARCHITECTURE §6). Server-owned rules stay in `domain/*.policy.ts` as reference impl + unit test, marked server-owned, 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 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`). - 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. - Prettier; `.editorconfig`. tsconfig: `noImplicitReturns`, `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 ``, 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.