feat(dx): gen:context generator (WP-44)

npm run gen:context scaffolds a bounded context: folders + starter page, the @<ctx>/*
tsconfig alias, a dependency-cruiser boundary entry, and a lazy authGuard route.

Refactors .dependency-cruiser.js's per-context contextRule calls into a single
CONTEXT_ALLOWED map that every rule derives from, so adding a context is really one
config entry (verified behavior-preserving: same dep:check counts, same graph output).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-27 14:18:43 +02:00
co-authored by Claude Sonnet 5
parent 7ef8ac7409
commit 7b6cabfc4a
8 changed files with 165 additions and 45 deletions
+31 -19
View File
@@ -8,16 +8,37 @@
// Allowed cross-context edges: everyone → shared; herregistratie → registratie; showcase → *
// (the sanctioned teaching page). Nobody imports showcase.
const FEATURES = 'auth|registratie|herregistratie|brief|beheer|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,
};
/** 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})/` },
});
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: [
@@ -29,16 +50,7 @@ module.exports = {
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'),
...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`.