feat(arch): WP-38 — dependency graph + declarative boundaries (dependency-cruiser)
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:
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: new-context
|
name: new-context
|
||||||
description: Scaffold a new DDD bounded context (folders, path alias, eslint boundary rules, lazy route). Use when adding a new business capability that doesn't belong in an existing context.
|
description: Scaffold a new DDD bounded context (folders, path alias, boundary rules, lazy route). Use when adding a new business capability that doesn't belong in an existing context.
|
||||||
---
|
---
|
||||||
|
|
||||||
# New bounded context
|
# New bounded context
|
||||||
@@ -18,16 +18,14 @@ shared/reusable code is English. The context name is the ubiquitous language ter
|
|||||||
only once it gets a wire seam). Empty layers can wait; don't scaffold placeholders.
|
only once it gets a wire seam). Empty layers can wait; don't scaffold placeholders.
|
||||||
2. **Path alias** — add `"@<ctx>/*": ["src/app/<ctx>/*"]` to `tsconfig.json` `paths`.
|
2. **Path alias** — add `"@<ctx>/*": ["src/app/<ctx>/*"]` to `tsconfig.json` `paths`.
|
||||||
Aliases are direction statements; always import cross-context via the alias.
|
Aliases are direction statements; always import cross-context via the alias.
|
||||||
3. **eslint boundaries** (`eslint.config.mjs`) — dependencies point inward and
|
3. **Boundaries** (`.dependency-cruiser.js`, WP-38 — the single declarative source;
|
||||||
toward `shared` only. Copy the existing per-context block (the `brief` block is
|
dependencies point inward and toward `shared` only). Add ONE `contextRule(...)` entry
|
||||||
the minimal leaf-context example) and:
|
for the new context listing the contexts it may **not** import (copy the `brief` leaf
|
||||||
- add a block for `src/app/<ctx>/**/*.ts` banning imports from every context it
|
example), and add the new context to the forbidden list of any context that must not
|
||||||
may **not** depend on;
|
depend on it. The layer rules (`domain/` framework-free, `contracts/` import-nothing,
|
||||||
- add `@<ctx>/*` to the ban lists of `shared/**` and every context that must not
|
ApiClient confinement, `ui ↛ infrastructure`) match by glob and cover it automatically.
|
||||||
depend on the new one (grep the config for `@brief/*` to find all lists);
|
Verify with `npm run dep:check`; regenerate the graph with `npm run dep:graph`. (Boundaries
|
||||||
- the generic blocks (`domain/**` framework-free, `contracts/**` import-nothing,
|
are no longer in `eslint.config.mjs` — that now holds only `no-explicit-any` + template a11y.)
|
||||||
ApiClient confinement, `ui/**` never imports `infrastructure`) match by glob
|
|
||||||
and cover the new context automatically.
|
|
||||||
4. **Route** — lazy child under the persistent shell in `app.routes.ts`:
|
4. **Route** — lazy child under the persistent shell in `app.routes.ts`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -40,12 +38,12 @@ shared/reusable code is English. The context name is the ubiquitous language ter
|
|||||||
## Worked example
|
## Worked example
|
||||||
|
|
||||||
`src/app/brief/` — an independent leaf context (depends only on shared): see its
|
`src/app/brief/` — an independent leaf context (depends only on shared): see its
|
||||||
folder layout and its eslint block in `eslint.config.mjs`.
|
folder layout and its `contextRule` entry in `.dependency-cruiser.js`.
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run lint && npm run build
|
npm run dep:check && npm run lint && npm run build
|
||||||
# prove the fence works: add a forbidden import (e.g. new ctx → @herregistratie/*),
|
# prove the fence works: add a forbidden import (e.g. new ctx → @herregistratie/*),
|
||||||
# confirm lint fails, remove it.
|
# confirm `npm run dep:check` fails, remove it.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
// Dependency-cruiser (WP-38): the single declarative source for the app's bounded-context
|
||||||
|
// + atomic-layer boundaries — and the graph you can SEE (`npm run dep:graph`). Replaces the
|
||||||
|
// hand-duplicated `no-restricted-imports` blocks that had to be copied per context (and that
|
||||||
|
// left `herregistratie` without one). ESLint keeps only the rules dep-cruiser can't express
|
||||||
|
// (no-explicit-any, template a11y).
|
||||||
|
//
|
||||||
|
// Contexts: shared (base) · auth · registratie · herregistratie · brief · beheer · showcase.
|
||||||
|
// Allowed cross-context edges: everyone → shared; herregistratie → registratie; showcase → *
|
||||||
|
// (the sanctioned teaching page). Nobody imports showcase.
|
||||||
|
|
||||||
|
const FEATURES = 'auth|registratie|herregistratie|brief|beheer|showcase';
|
||||||
|
|
||||||
|
/** A context may import shared + itself; this lists the OTHER contexts it may NOT import. */
|
||||||
|
const contextRule = (name, from, forbiddenContexts) => ({
|
||||||
|
name,
|
||||||
|
comment: `${from} may depend only on its allowed contexts (+ shared). See CLAUDE.md §1.`,
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: `^src/app/${from}/` },
|
||||||
|
to: { path: `^src/app/(${forbiddenContexts})/` },
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
forbidden: [
|
||||||
|
// --- Bounded-context direction (the "dependencies point inward" spine) ---
|
||||||
|
{
|
||||||
|
name: 'shared-no-features',
|
||||||
|
comment: 'shared/ is the base — it must not import any feature context.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '^src/app/shared/', pathNot: '^src/app/shared/ui/debug-state/' },
|
||||||
|
to: { path: `^src/app/(${FEATURES})/` },
|
||||||
|
},
|
||||||
|
contextRule('auth-only-shared', 'auth', 'registratie|herregistratie|brief|beheer|showcase'),
|
||||||
|
contextRule(
|
||||||
|
'registratie-only-shared',
|
||||||
|
'registratie',
|
||||||
|
'auth|herregistratie|brief|beheer|showcase',
|
||||||
|
),
|
||||||
|
// herregistratie MAY import registratie (+ shared) — the one sanctioned cross-feature edge.
|
||||||
|
contextRule('herregistratie-scope', 'herregistratie', 'auth|brief|beheer|showcase'),
|
||||||
|
contextRule('brief-only-shared', 'brief', 'auth|registratie|herregistratie|beheer|showcase'),
|
||||||
|
contextRule('beheer-only-shared', 'beheer', 'auth|registratie|herregistratie|brief|showcase'),
|
||||||
|
// showcase/ is exempt (reads every context by design); nothing imports it — covered by the
|
||||||
|
// rules above each forbidding `→ showcase`.
|
||||||
|
|
||||||
|
// --- Atomic-layer rules (dependencies point inward: ui → application → domain) ---
|
||||||
|
{
|
||||||
|
name: 'domain-is-pure',
|
||||||
|
comment: 'domain/ is framework-free business logic — no Angular.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '/domain/' },
|
||||||
|
to: { path: 'node_modules/@angular/' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'contracts-import-nothing',
|
||||||
|
comment: 'contracts/ are pure wire DTO shapes — they import nothing (ADR-0001).',
|
||||||
|
severity: 'error',
|
||||||
|
from: { path: '/contracts/' },
|
||||||
|
to: { pathNot: '/contracts/', path: '^(src/app/|node_modules/@angular/)' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ui-not-infrastructure',
|
||||||
|
comment:
|
||||||
|
'ui/ + layout/ reach data through an application store/command, never infrastructure directly (type-only DTO imports allowed).',
|
||||||
|
severity: 'error',
|
||||||
|
from: {
|
||||||
|
path: '(/ui/|/layout/)',
|
||||||
|
pathNot: '\\.stories\\.ts$|\\.spec\\.ts$|^src/app/shared/ui/debug-state/',
|
||||||
|
},
|
||||||
|
to: { path: '/infrastructure/', dependencyTypesNot: ['type-only'] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'apiclient-infrastructure-only',
|
||||||
|
comment:
|
||||||
|
'The generated ApiClient is a value only inside infrastructure/ (+ shared/upload); elsewhere type-only.',
|
||||||
|
severity: 'error',
|
||||||
|
from: { pathNot: '/infrastructure/|^src/app/shared/upload/' },
|
||||||
|
to: {
|
||||||
|
path: '^src/app/shared/infrastructure/api-client\\.ts$',
|
||||||
|
dependencyTypesNot: ['type-only'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- Hygiene (cheap wins a graph makes obvious) ---
|
||||||
|
{
|
||||||
|
name: 'no-circular',
|
||||||
|
comment: 'No cyclic dependencies.',
|
||||||
|
severity: 'error',
|
||||||
|
from: {},
|
||||||
|
to: { circular: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
options: {
|
||||||
|
doNotFollow: { path: 'node_modules' },
|
||||||
|
tsConfig: { fileName: 'tsconfig.json' }, // resolves @shared/@registratie/… path aliases
|
||||||
|
tsPreCompilationDeps: true, // needed so `type-only` imports are distinguished
|
||||||
|
enhancedResolveOptions: {
|
||||||
|
exportsFields: ['exports'],
|
||||||
|
conditionNames: ['import', 'require', 'node', 'default'],
|
||||||
|
},
|
||||||
|
reporterOptions: {
|
||||||
|
// Context-level architecture graph for `npm run dep:graph` (mermaid — no graphviz needed).
|
||||||
|
archi: { collapsePattern: '^src/app/[^/]+/[^/]+' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -27,6 +27,8 @@ jobs:
|
|||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci --prefer-offline --no-audit --no-fund
|
- run: npm ci --prefer-offline --no-audit --no-fund
|
||||||
- run: npm run lint
|
- run: npm run lint
|
||||||
|
# Bounded-context + atomic-layer boundaries (WP-38, dependency-cruiser).
|
||||||
|
- run: npm run dep:check
|
||||||
- run: npm run format:check
|
- run: npm run format:check
|
||||||
- run: npm run check:tokens
|
- run: npm run check:tokens
|
||||||
- run: npm test
|
- run: npm test
|
||||||
|
|||||||
@@ -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-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-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-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-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-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 |
|
| [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
|
# WP-38 — Dependency graph + declarative boundaries
|
||||||
|
|
||||||
Status: todo
|
Status: done
|
||||||
Phase: 8 — platform/DX/showcase
|
Phase: 8 — platform/DX/showcase
|
||||||
Priority: P1
|
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
|
## Why
|
||||||
|
|
||||||
Bounded-context + atomic-layer boundaries are enforced only by hand-duplicated
|
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
|
## Acceptance criteria
|
||||||
|
|
||||||
- [ ] One declarative config expresses all allowed context/layer edges; herregistratie included.
|
- [x] 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`.
|
- [x] `npm run dep:graph` produces a committed mermaid architecture graph; `dep:check` runs in `npm run ci`.
|
||||||
- [ ] A deliberately-illegal import fails the validate step (proven, then reverted).
|
- [x] 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] 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
|
||||||
|
```
|
||||||
+9
-225
@@ -2,11 +2,15 @@ import tseslint from 'typescript-eslint';
|
|||||||
import angular from 'angular-eslint';
|
import angular from 'angular-eslint';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces the architecture's working agreements that were previously only
|
* ESLint now owns only the rules that are NOT dependency-graph shaped: no `any`, and
|
||||||
* documented (CLAUDE.md): no `any`, domain/ stays framework-free, and the
|
* template accessibility. The architecture's **boundary** rules — bounded-context
|
||||||
* dependency direction between contexts (herregistratie → registratie → shared,
|
* direction (herregistratie → registratie → shared, auth/brief/beheer → shared; shared
|
||||||
* auth → shared; shared depends on nothing). Boundary rules use path patterns on
|
* depends on nothing), `domain/` framework-freedom, `contracts/` purity, `ui ↛
|
||||||
* the import aliases, so they read as the direction statement they enforce.
|
* infrastructure`, and ApiClient confinement — moved to **dependency-cruiser** (WP-38,
|
||||||
|
* `.dependency-cruiser.js`): one declarative source that also emits the architecture graph
|
||||||
|
* (`npm run dep:graph`) and is enforced by `npm run dep:check` (in CI). That replaced the
|
||||||
|
* per-context `no-restricted-imports` blocks that had to be hand-copied (and had left
|
||||||
|
* `herregistratie` uncovered).
|
||||||
*/
|
*/
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -46,224 +50,4 @@ export default [
|
|||||||
files: ['src/**/*.spec.ts'],
|
files: ['src/**/*.spec.ts'],
|
||||||
rules: { '@typescript-eslint/no-explicit-any': 'off' },
|
rules: { '@typescript-eslint/no-explicit-any': 'off' },
|
||||||
},
|
},
|
||||||
|
|
||||||
// domain/ = pure business rules + types. No Angular, ever.
|
|
||||||
{
|
|
||||||
files: ['src/app/**/domain/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@angular/*', '@angular/**'],
|
|
||||||
message: 'domain/ must stay framework-free (pure TS) — no Angular imports.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// shared/ is the base layer: it may not depend on any feature context.
|
|
||||||
// The dev-only debug panel is the sanctioned exception (it observes every store).
|
|
||||||
{
|
|
||||||
files: ['src/app/shared/**/*.ts'],
|
|
||||||
ignores: ['src/app/shared/ui/debug-state/**'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@auth/*', '@registratie/*', '@herregistratie/*', '@brief/*', '@beheer/*'],
|
|
||||||
message: 'shared/ must not depend on a feature context.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// auth/ may depend only on shared.
|
|
||||||
{
|
|
||||||
files: ['src/app/auth/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@registratie/*', '@herregistratie/*', '@brief/*', '@beheer/*'],
|
|
||||||
message: 'auth/ may depend only on shared.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// registratie/ may depend on shared, not on herregistratie (direction points the other way).
|
|
||||||
{
|
|
||||||
files: ['src/app/registratie/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@herregistratie/*', '@brief/*', '@beheer/*'],
|
|
||||||
message: 'Dependencies point herregistratie → registratie → shared, never back.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// brief/ (letter composition) is an independent leaf context: it may depend only on shared.
|
|
||||||
{
|
|
||||||
files: ['src/app/brief/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@auth/*', '@registratie/*', '@herregistratie/*', '@beheer/*'],
|
|
||||||
message: 'brief/ may depend only on shared.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// beheer/ (stamdata maintenance) is an independent leaf context: it may depend only on shared.
|
|
||||||
{
|
|
||||||
files: ['src/app/beheer/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@auth/*', '@registratie/*', '@herregistratie/*', '@brief/*'],
|
|
||||||
message: 'beheer/ may depend only on shared.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// contracts/ is the FE⇄BE wire seam: pure DTO shapes that must import NOTHING
|
|
||||||
// (CLAUDE.md §1, ADR-0001) — not Angular, not a context alias, not relative app
|
|
||||||
// code. Enums are inlined string-literal unions; the adapter's parse* maps them.
|
|
||||||
// (This comes after the per-context rules so it wins for contracts files.)
|
|
||||||
{
|
|
||||||
files: ['src/app/**/contracts/**/*.ts'],
|
|
||||||
rules: {
|
|
||||||
'no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
'@angular/**',
|
|
||||||
'@shared/**',
|
|
||||||
'@auth/**',
|
|
||||||
'@registratie/**',
|
|
||||||
'@herregistratie/**',
|
|
||||||
'@brief/**',
|
|
||||||
'@beheer/**',
|
|
||||||
'./*',
|
|
||||||
'../*',
|
|
||||||
'./**',
|
|
||||||
'../**',
|
|
||||||
],
|
|
||||||
message:
|
|
||||||
'contracts/ is the wire seam — it must import NOTHING (pure DTO shapes). Map wire → domain in the infrastructure adapter, not here.',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// BFF-lite anti-corruption boundary (ADR-0001): the ApiClient (the network
|
|
||||||
// client) may be imported as a VALUE only from infrastructure-role files.
|
|
||||||
// Type-only imports of generated wire DTOs are allowed anywhere — they grant no
|
|
||||||
// network access. UI/application reach the network through an adapter or command.
|
|
||||||
{
|
|
||||||
files: ['src/app/**/*.ts'],
|
|
||||||
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: ['@shared/infrastructure/api-client'],
|
|
||||||
allowTypeImports: true,
|
|
||||||
message:
|
|
||||||
'The ApiClient lives only in infrastructure/ adapters (ADR-0001). UI/application call an adapter or a command, not the network client. (Type-only DTO imports are fine: use `import type`.)',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// …the infrastructure adapters ARE that boundary and own the client. shared/upload
|
|
||||||
// is a feature-scoped adapter that lives outside a /infrastructure/ folder.
|
|
||||||
{
|
|
||||||
files: ['src/app/**/infrastructure/**/*.ts', 'src/app/shared/upload/**/*.ts'],
|
|
||||||
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
||||||
rules: { '@typescript-eslint/no-restricted-imports': 'off' },
|
|
||||||
},
|
|
||||||
|
|
||||||
// ui/ and layout/ are the presentation layer: dependencies point inward
|
|
||||||
// (ui → application → domain, CLAUDE.md §1), so they must NOT import
|
|
||||||
// infrastructure/ directly — they reach data through an application store or
|
|
||||||
// command. (Stories/specs are test scaffolding and may wire the real client.)
|
|
||||||
// Uses the @typescript-eslint variant so it composes with the base
|
|
||||||
// no-restricted-imports context-direction rules above (last-wins is per rule name).
|
|
||||||
{
|
|
||||||
files: ['src/app/**/ui/**/*.ts', 'src/app/**/layout/**/*.ts'],
|
|
||||||
// debug-state is the sanctioned devtool (same precedent as the cross-context
|
|
||||||
// exemption above): its WP-33 role/scenario switchers write the infrastructure
|
|
||||||
// dev-mechanism helpers directly. Never a product feature — isDevMode()-gated.
|
|
||||||
ignores: ['**/*.stories.ts', '**/*.spec.ts', 'src/app/shared/ui/debug-state/**'],
|
|
||||||
plugins: { '@typescript-eslint': tseslint.plugin },
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/no-restricted-imports': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
patterns: [
|
|
||||||
{
|
|
||||||
group: [
|
|
||||||
'@shared/infrastructure/*',
|
|
||||||
'@auth/infrastructure/*',
|
|
||||||
'@registratie/infrastructure/*',
|
|
||||||
'@herregistratie/infrastructure/*',
|
|
||||||
'@brief/infrastructure/*',
|
|
||||||
'@beheer/infrastructure/*',
|
|
||||||
],
|
|
||||||
allowTypeImports: true,
|
|
||||||
message:
|
|
||||||
'ui/ and layout/ must not import infrastructure/ directly (CLAUDE.md §1: ui → application → domain). Reach data through an application store or command. (Type-only DTO imports are fine: use `import type`.)',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Sanctioned exception: showcase/ is the teaching page whose whole point is showing
|
|
||||||
// multiple contexts side by side (ARCHITECTURE.md §6). It may read every context;
|
|
||||||
// nothing imports showcase. Same precedent as the shared/ui/debug-state exemption.
|
|
||||||
{
|
|
||||||
files: ['src/app/showcase/**/*.ts'],
|
|
||||||
rules: { 'no-restricted-imports': 'off' },
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
Generated
+220
-3
@@ -36,6 +36,7 @@
|
|||||||
"angular-eslint": "^22.0.0",
|
"angular-eslint": "^22.0.0",
|
||||||
"axe-playwright": "^2.2.2",
|
"axe-playwright": "^2.2.2",
|
||||||
"concurrently": "^10.0.3",
|
"concurrently": "^10.0.3",
|
||||||
|
"dependency-cruiser": "^18.1.0",
|
||||||
"eslint": "^10.6.0",
|
"eslint": "^10.6.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"jsdom": "^29.0.0",
|
"jsdom": "^29.0.0",
|
||||||
@@ -10872,6 +10873,39 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-jsx-walk": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-jsx-walk/-/acorn-jsx-walk-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-uuo6iJj4D4ygkdzd6jPtcxs8vZgDX9YFIkqczGImoypX2fQ4dVImmu3UzA4ynixCIMTrEOWW+95M2HuBaCEOVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/acorn-loose": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.15.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz",
|
||||||
|
"integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/adjust-sourcemap-loader": {
|
"node_modules/adjust-sourcemap-loader": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz",
|
||||||
@@ -13254,6 +13288,77 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dependency-cruiser": {
|
||||||
|
"version": "18.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-18.1.0.tgz",
|
||||||
|
"integrity": "sha512-pbsH0gQ15BxXi97SCuMeVgsh8VzYpziGIVaMdnALbVu+TYjSZrW9/nOvsPU+NjHLmJSF9R1UijjoACq6Ne/ybQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "8.17.0",
|
||||||
|
"acorn-jsx": "5.3.2",
|
||||||
|
"acorn-jsx-walk": "2.0.0",
|
||||||
|
"acorn-loose": "8.5.2",
|
||||||
|
"acorn-walk": "8.3.5",
|
||||||
|
"commander": "15.0.0",
|
||||||
|
"enhanced-resolve": "5.24.2",
|
||||||
|
"ignore": "7.0.6",
|
||||||
|
"interpret": "3.1.1",
|
||||||
|
"is-installed-globally": "1.0.0",
|
||||||
|
"json5": "2.2.3",
|
||||||
|
"picomatch": "4.0.5",
|
||||||
|
"prompts": "2.4.2",
|
||||||
|
"rechoir": "0.8.0",
|
||||||
|
"safe-regex": "2.1.1",
|
||||||
|
"semver": "7.8.5",
|
||||||
|
"tsconfig-paths-webpack-plugin": "4.2.0",
|
||||||
|
"watskeburt": "6.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"depcruise": "bin/dependency-cruise.mjs",
|
||||||
|
"depcruise-baseline": "bin/depcruise-baseline.mjs",
|
||||||
|
"depcruise-fmt": "bin/depcruise-fmt.mjs",
|
||||||
|
"depcruise-wrap-stream-in-html": "bin/wrap-stream-in-html.mjs",
|
||||||
|
"dependency-cruise": "bin/dependency-cruise.mjs",
|
||||||
|
"dependency-cruiser": "bin/dependency-cruise.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22||^24||>=26"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dependency-cruiser/node_modules/commander": {
|
||||||
|
"version": "15.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz",
|
||||||
|
"integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=22.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dependency-cruiser/node_modules/ignore": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dependency-cruiser/node_modules/semver": {
|
||||||
|
"version": "7.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
|
||||||
|
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
@@ -13639,9 +13744,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.24.1",
|
"version": "5.24.2",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.1.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.2.tgz",
|
||||||
"integrity": "sha512-7DdUaTjmNwMcH2gLr1qycesKII3BK4RLy/mdAb7x10Lq7bR4aNKHt1BR1ZALSv0rPM/hF5wYF0PhGop/rJm8vw==",
|
"integrity": "sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -15299,6 +15404,32 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/global-directory": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ini": "4.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/global-directory/node_modules/ini": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/global-modules": {
|
"node_modules/global-modules": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz",
|
||||||
@@ -16123,6 +16254,16 @@
|
|||||||
"node": "^20.17.0 || >=22.9.0"
|
"node": "^20.17.0 || >=22.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/interpret": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ip-address": {
|
"node_modules/ip-address": {
|
||||||
"version": "10.2.0",
|
"version": "10.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
@@ -16276,6 +16417,23 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-installed-globally": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"global-directory": "^4.0.1",
|
||||||
|
"is-path-inside": "^4.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-interactive": {
|
"node_modules/is-interactive": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
@@ -16312,6 +16470,19 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-path-inside": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-obj": {
|
"node_modules/is-plain-obj": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
|
||||||
@@ -22199,6 +22370,19 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rechoir": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve": "^1.20.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/redent": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
@@ -22247,6 +22431,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/regexp-tree": {
|
||||||
|
"version": "0.1.27",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||||
|
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"regexp-tree": "bin/regexp-tree"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/regexpu-core": {
|
"node_modules/regexpu-core": {
|
||||||
"version": "6.4.0",
|
"version": "6.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz",
|
||||||
@@ -22878,6 +23072,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/safe-regex": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"regexp-tree": "~0.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -25802,6 +26006,19 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/watskeburt": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/watskeburt/-/watskeburt-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-jfiuDABaxSkC71T6oZ3vCS99roYkSHm/+As+G0Dz8taAHQb+SJBvLEm5RlsgG71XdfAj3rv7eudUBTgwcQUPlQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"watskeburt": "dist/run-cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.13||^24||>=26"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wbuf": {
|
"node_modules/wbuf": {
|
||||||
"version": "1.7.3",
|
"version": "1.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz",
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
"test-storybook": "test-storybook",
|
"test-storybook": "test-storybook",
|
||||||
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
"test-storybook:ci": "concurrently -k -s first -n sb,axe \"http-server storybook-static -p 6006 --silent\" \"wait-on tcp:127.0.0.1:6006 && test-storybook --url http://127.0.0.1:6006\"",
|
||||||
"check:tokens": "bash scripts/check-tokens.sh",
|
"check:tokens": "bash scripts/check-tokens.sh",
|
||||||
|
"dep:check": "depcruise src/app --config .dependency-cruiser.js",
|
||||||
|
"dep:graph": "bash scripts/dep-graph.sh",
|
||||||
"ci": "bash scripts/ci-local.sh",
|
"ci": "bash scripts/ci-local.sh",
|
||||||
"e2e": "playwright test",
|
"e2e": "playwright test",
|
||||||
"extract-i18n": "ng extract-i18n --output-path src/locale"
|
"extract-i18n": "ng extract-i18n --output-path src/locale"
|
||||||
@@ -51,6 +53,7 @@
|
|||||||
"angular-eslint": "^22.0.0",
|
"angular-eslint": "^22.0.0",
|
||||||
"axe-playwright": "^2.2.2",
|
"axe-playwright": "^2.2.2",
|
||||||
"concurrently": "^10.0.3",
|
"concurrently": "^10.0.3",
|
||||||
|
"dependency-cruiser": "^18.1.0",
|
||||||
"eslint": "^10.6.0",
|
"eslint": "^10.6.0",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"jsdom": "^29.0.0",
|
"jsdom": "^29.0.0",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ cd "$(dirname "$0")/.."
|
|||||||
step() { printf '\n\033[1;36m▶ %s\033[0m\n' "$1"; }
|
step() { printf '\n\033[1;36m▶ %s\033[0m\n' "$1"; }
|
||||||
|
|
||||||
step "lint"; npm run lint
|
step "lint"; npm run lint
|
||||||
|
step "dependency boundaries"; npm run dep:check
|
||||||
step "format:check (prettier)"; npm run format:check
|
step "format:check (prettier)"; npm run format:check
|
||||||
step "check:tokens"; npm run check:tokens
|
step "check:tokens"; npm run check:tokens
|
||||||
step "test (vitest)"; npm test
|
step "test (vitest)"; npm test
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Regenerate the architecture dependency graph (WP-38) as a mermaid diagram embedded in a
|
||||||
|
# markdown file, so it renders on the git host and can be linked from the docs. Contexts ×
|
||||||
|
# atomic layers; edges are real imports. Boundaries are enforced separately by `npm run
|
||||||
|
# dep:check`. Run this after a structural change; commit the result.
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
OUT=docs/reference/architecture/dependency-graph.md
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Dependency graph"
|
||||||
|
echo
|
||||||
|
echo "_Generated by \`npm run dep:graph\` — do not edit by hand._ Nodes are context × atomic"
|
||||||
|
echo "layer (\`src/app/<context>/<layer>\`); edges are real imports. The allowed edges are"
|
||||||
|
echo "enforced by \`npm run dep:check\` (dependency-cruiser); see [dependencies.md](./dependencies.md)."
|
||||||
|
echo
|
||||||
|
echo '```mermaid'
|
||||||
|
npx depcruise src/app \
|
||||||
|
--config .dependency-cruiser.js \
|
||||||
|
--include-only "^src/app/" \
|
||||||
|
--collapse "^src/app/[^/]+/[^/]+" \
|
||||||
|
--output-type mermaid
|
||||||
|
echo '```'
|
||||||
|
} >"$OUT"
|
||||||
|
|
||||||
|
echo "wrote $OUT"
|
||||||
Reference in New Issue
Block a user