// 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. // Single source of truth for bounded-context boundaries: each entry maps a context name to the // OTHER contexts it may additionally import (besides itself + shared). `showcase` maps to `null` // — sanctioned to import every context (the teaching page); nothing else may import it. Add a // context here — nowhere else — when scaffolding one (see `gen:context`, WP-44); FEATURES and // every contextRule below are derived from this object. const CONTEXT_ALLOWED = { auth: [], registratie: [], herregistratie: ['registratie'], // the one sanctioned cross-feature edge brief: [], beheer: [], showcase: null, }; const FEATURES = Object.keys(CONTEXT_ALLOWED).join('|'); /** A context may import shared + itself + its allowed list; forbidden = every other context. */ const contextRule = (from) => { const allowed = CONTEXT_ALLOWED[from]; if (allowed === null) return null; // unrestricted (showcase) — no rule to generate const forbidden = Object.keys(CONTEXT_ALLOWED) .filter((name) => name !== from && !allowed.includes(name)) .join('|'); return { name: `${from}-scope`, 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/(${forbidden})/` }, }; }; 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})/` }, }, ...Object.keys(CONTEXT_ALLOWED).map(contextRule).filter(Boolean), // 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/[^/]+/[^/]+' }, }, }, };