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:
@@ -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/[^/]+/[^/]+' },
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user