feat(arch): WP-38 — dependency graph + declarative boundaries (dependency-cruiser)
CI / frontend (push) Successful in 2m11s
CI / storybook-a11y (push) Successful in 5m46s
CI / backend (push) Successful in 1m29s
CI / e2e (push) Successful in 2m55s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 2m5s

Adopt dependency-cruiser as the single declarative source for bounded-context +
atomic-layer boundaries, replacing the per-context no-restricted-imports blocks that
had to be hand-copied (and had left herregistratie uncovered). `.dependency-cruiser.js`
encodes context direction (everyone→shared, herregistratie→registratie, showcase→*),
domain-purity, contracts-import-nothing, ui↛infrastructure, ApiClient confinement, and
no-circular. `npm run dep:check` enforces (wired into ci-local.sh + the frontend CI job);
`npm run dep:graph` emits a committed mermaid context×layer graph. ESLint slimmed to
no-explicit-any + template a11y. Docs + new-context skill updated to the single source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 13:51:04 +02:00
co-authored by Claude Opus 4.8
parent fe9e3121c7
commit 7d2a36ff22
12 changed files with 641 additions and 248 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ for its existing violations, so every WP ends green.
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | todo |
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | todo |
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | todo |
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | todo |
@@ -1,9 +1,20 @@
# WP-38 — Dependency graph + declarative boundaries
Status: todo
Status: done
Phase: 8 — platform/DX/showcase
Priority: P1
## Outcome
Adopted **dependency-cruiser**. `.dependency-cruiser.js` is the single declarative source for
context + layer boundaries (incl. the previously-missing `herregistratie` scope + no-circular);
`npm run dep:check` enforces (wired into `ci-local.sh` + the `frontend` CI job), `npm run dep:graph`
emits a mermaid context×layer graph to `docs/reference/architecture/dependency-graph.md`. The
per-context `no-restricted-imports` blocks were **removed** from `eslint.config.mjs` (now only
`no-explicit-any` + template a11y remain); parity verified by planting violations (domain→Angular,
beheer→registratie incl. type-only) and confirming `dep:check` flags them. Doc:
`docs/reference/architecture/dependencies.md`; `new-context` skill updated to the single source.
## Why
Bounded-context + atomic-layer boundaries are enforced only by hand-duplicated
@@ -32,7 +43,7 @@ showcase`; layers `domain/application/infrastructure/contracts/ui`); **fix the h
## Acceptance criteria
- [ ] One declarative config expresses all allowed context/layer edges; herregistratie included.
- [ ] `npm run graph` produces an architecture graph (SVG/HTML); validate runs in `npm run ci`.
- [ ] A deliberately-illegal import fails the validate step (proven, then reverted).
- [ ] No loss of enforcement vs the old ESLint blocks; `npm run ci` green.
- [x] One declarative config expresses all allowed context/layer edges; herregistratie included.
- [x] `npm run dep:graph` produces a committed mermaid architecture graph; `dep:check` runs in `npm run ci`.
- [x] A deliberately-illegal import fails the validate step (proven, then reverted).
- [x] No loss of enforcement vs the old ESLint blocks; `npm run ci` green.
@@ -0,0 +1,57 @@
# Dependencies & boundaries
How the app's **bounded-context** and **atomic-layer** boundaries are declared, enforced, and
visualized (WP-38). One declarative source — `.dependency-cruiser.js` — both **guards** the edges
and **draws** the graph, replacing the per-context `no-restricted-imports` blocks that previously
had to be hand-copied (and that had left `herregistratie` uncovered).
## The rules (single source: `.dependency-cruiser.js`)
**Bounded-context direction** — dependencies point inward; everyone may use `shared`, nothing
imports `showcase`:
| Context | May import |
| ---------------- | ------------------------------------- |
| `shared` | (base — no feature context) |
| `auth` | `shared` |
| `registratie` | `shared` |
| `herregistratie` | `registratie`, `shared` |
| `brief` | `shared` |
| `beheer` | `shared` |
| `showcase` | everything (sanctioned teaching page) |
**Atomic-layer rules:** `domain/` is framework-free (no Angular); `contracts/` import nothing
(pure wire DTOs, ADR-0001); `ui/` + `layout/` never import `infrastructure/` directly (reach data
through an application store/command — type-only DTO imports are fine); the generated `ApiClient`
is a value only inside `infrastructure/` (+ `shared/upload`). Plus **no circular** dependencies.
Sanctioned exceptions: `shared/ui/debug-state` (dev panel) and `showcase`.
## See the graph
```bash
npm run dep:graph # regenerates docs/reference/architecture/dependency-graph.md (mermaid)
```
[dependency-graph.md](./dependency-graph.md) is the generated, committed view — contexts × atomic
layers, edges are real imports. It renders on the git host; regenerate + commit after a structural
change.
## Enforce
```bash
npm run dep:check # fails on any forbidden edge; part of `npm run ci` and CI
```
A violation prints the offending `from → to` and the rule name. `dep:check` runs in the local gate
(`scripts/ci-local.sh`) and the `frontend` CI job.
## What still lives in ESLint
Only the non-dependency rules: `@typescript-eslint/no-explicit-any` and the angular-eslint template
accessibility bundle (see `eslint.config.mjs`). Everything about _who may import whom_ is in
dependency-cruiser.
## Adding a context
Add one `contextRule(...)` entry in `.dependency-cruiser.js` (and the tsconfig path alias + lazy
route) — no more hand-copying ESLint blocks. The `new-context` skill covers the full checklist.
@@ -0,0 +1,187 @@
# Dependency graph
_Generated by `npm run dep:graph` — do not edit by hand._ Nodes are context × atomic
layer (`src/app/<context>/<layer>`); edges are real imports. The allowed edges are
enforced by `npm run dep:check` (dependency-cruiser); see [dependencies.md](./dependencies.md).
```mermaid
flowchart LR
subgraph 0["src"]
subgraph 1["app"]
2["app.config.ts"]
3["app.routes.ts"]
4["app.ts"]
subgraph 5["auth"]
6["application"]
7["auth.guard.spec.ts"]
8["auth.guard.ts"]
9["domain"]
A["infrastructure"]
B["ui"]
end
subgraph C["beheer"]
D["application"]
E["contracts"]
F["domain"]
G["infrastructure"]
H["ui"]
end
subgraph I["brief"]
J["application"]
K["domain"]
L["infrastructure"]
M["ui"]
end
subgraph N["herregistratie"]
O["application"]
P["domain"]
Q["infrastructure"]
R["ui"]
end
subgraph S["registratie"]
T["application"]
U["contracts"]
V["domain"]
W["infrastructure"]
X["ui"]
end
subgraph Y["shared"]
Z["application"]
10["domain"]
11["infrastructure"]
12["kernel"]
13["layout"]
14["ui"]
15["upload"]
end
subgraph 16["showcase"]
17["concepts.page.ts"]
end
end
end
2-->3
2-->6
2-->Z
2-->11
2-->13
3-->17
3-->8
3-->B
3-->H
3-->M
3-->R
3-->X
3-->Z
3-->13
6-->9
6-->A
6-->12
7-->6
7-->8
7-->Z
8-->6
8-->Z
8-->10
A-->9
A-->12
B-->6
B-->13
B-->14
D-->F
D-->G
D-->Z
D-->12
F-->12
G-->F
G-->Z
G-->11
G-->12
H-->D
H-->Z
H-->13
H-->14
H-->F
J-->K
J-->L
J-->Z
J-->12
J-->15
K-->12
K-->15
L-->K
L-->Z
L-->11
L-->12
M-->J
M-->13
M-->14
M-->K
M-->12
M-->Z
M-->15
O-->P
O-->Q
P-->V
P-->12
P-->15
Q-->P
Q-->11
Q-->12
R-->P
R-->T
R-->Z
R-->12
R-->13
R-->14
R-->15
R-->O
R-->V
R-->11
T-->U
T-->V
T-->W
T-->Z
T-->11
T-->12
V-->12
V-->15
W-->V
W-->11
W-->12
W-->U
X-->V
X-->14
X-->T
X-->13
X-->Z
X-->12
X-->U
X-->15
X-->11
Z-->12
Z-->11
Z-->10
11-->10
11-->12
13-->14
13-->10
13-->Z
14-->15
14-->Z
14-->12
14-->6
14-->9
14-->T
14-->10
14-->11
14-->V
15-->12
15-->Z
15-->11
17-->R
17-->V
17-->X
17-->13
17-->14
```