Skip to content

Imports

imports states what this node and everything under it may reach. It is the family that carries dependency-direction policy: which tiers may see which, and which may not.

"@/modules/{module}/domain/":
imports:
message: "domain/ holds the model — no framework, no persistence, no bus."
reset: true
external: [effect]
allow: ["@/modules/{module}/domain/**", "@/platform/ddd/contracts/**"]
deny:
- match: "@/modules/*/index.ts"
message: "Cross-module access goes through a port, not another module's barrel."
Field Meaning
allow repo-relative globs. A resolved target matching none of them is the violation.
external npm package names. Omit to inherit; [] forbids every external.
deny prohibitions checked before allow, and winning over it.
message what the allowlist means, shown when a target falls outside it.
reset stop inheriting ancestors’ allowances.
unrestricted this tier has no allowlist yet.

Every entry is matched against the resolved target — the real file the specifier names after paths, workspace links and package exports have been applied. A target matching no entry is the violation, which means widening is something you do by name, in the open, in a diff someone reviews.

Local and external targets are separate channels. allow governs files inside the repository; external governs npm packages, matched by package name:

external: [effect, "@effect-server-utils/cqrs"]

external: [] is meaningful and strong — “this tier reaches no third-party code at all”. Omitting external inherits the nearest ancestor’s list, which is usually what you want: one tier states the runtime it is allowed to name, and its subtree lives with it.

A prohibition is checked first and wins over allow. That is not about strictness — a target outside the allowlist is already refused — it is about the message. deny is how a rule keeps a specific, useful sentence for a mistake that the allowlist would otherwise report generically:

deny:
- match: "packages/*/src/**"
matchNot: "packages/contracts/src/**"
except: "**/*.test.ts"
message: "A domain type may not cross a package boundary except through contracts."
Field Meaning
match the target this prohibits
matchNot targets carved back out of match
except importers the prohibition does not apply to
message why

except is the important asymmetry: an exemption to a prohibition is declared by the prohibition, in the same breath, never by the tier escaping it. A descendant cannot opt itself out of a rule an ancestor wrote. See Inheritance.

Prohibitions that hold everywhere go in the top-level deny array instead of on a node.

Both exist so that a loose tier is a sentence someone wrote rather than a gap nobody noticed, and both are greppable.

reset: true stops inheriting ancestors’ allow and external. A tier that is genuinely narrower than its parent — a domain layer inside an application package — states its own allowlist from scratch rather than trying to subtract. Inherited deny entries are unaffected: a prohibition only ever accumulates.

unrestricted: true says “this tier has no allowlist yet; only the prohibitions apply.” It implies reset. It is required whenever a node states imports without an allow — you cannot leave the allowlist off silently. That is the point: grep unrestricted over a manifest is the adoption backlog, and the number should go down.