core/depcruise — layers & fences for imports
Declare your architecture as named layers and the fences between them; core/depcruise compiles that to dependency-cruiser’s native config and blocks a crossed import. Config you read, not regexes you decode.
What it catches
Import-boundary violations — the functional-core / imperative-shell split, feature isolation, “gate first” invariants. It orchestrates dependency-cruiser (never reimplements the graph walk — the same doctrine as ast-grep). It’s a whole-graph check, so it runs at commit (and turn), not per keystroke.
Config
rules:
- id: import-fence
use: core/depcruise
layers: # every name is yours — nothing built in
core: ["src/lib/**/domain.ts"] # path globs
effects: ["src/lib/**/db.ts", "src/lib/**/client.ts"]
shell: ["src/pages/**"]
pure-libs: ["zod", "date-fns"] # bare names = npm packages
node-fx: ["node:*"] # Node builtins are effects
only: # FAIL-CLOSED: core may import ONLY these
core: [core, pure-libs] # an unlisted import blocks, with a message
except: [type-only] # `import type {…}` runs nothing — always allowed
forbid:
- { from: core, to: effects, transitive: true, why: "purity is what core can REACH" }
- { from: core, to: node-fx, why: "Node builtins are effects" }
- circular
- orphans
require: # INVERTED: these modules MUST import that
- { in: "src/pages/api/**", import: "src/pages/api/_runtime", why: "gate through requireSession FIRST" }
warn: [sdp] # Stable Dependencies Principle — informs, never blocks
message: "An import crossed a fence — the named check above says which, and why."
What each affordance buys
| Affordance | Closes |
|---|---|
only: (allowlist) |
The fail-open hole. A blocklist of effectful packages decays silently; an allowlist fails closed — the first unvetted import blocks with a message saying where to add it. |
transitive: true |
Effect-laundering. domain → helper → db passes when only direct imports are checked. Purity is about what core can reach. |
node-fx layer |
The builtins hole — node:fs in a domain file passes every naïve fence. |
except: [type-only] |
Type duplication. A type-only import is erased at compile time; it never becomes an edge, so it’s transparent to every fence. |
require: |
Invariants stated only in prose (“gate FIRST”) become checked. |
The native escape hatch
The dialect lifts the small, stable slice of dependency-cruiser that covers the real use. For the long tail of its schema, point config: at a raw .cjs and it runs untouched:
- id: import-fence
use: core/depcruise
config: fcis/import-fence.cjs # a native dependency-cruiser config, run verbatim
Baseline dependency. dependency-cruiser (and a compatible typescript) ship as engine baseline deps, beside @ast-grep/napi — a core script whose tool might be missing would be a broken promise. Scanning defaults to src; set scan: to point elsewhere.