Skip to content

Inheritance

A manifest is a tree, so a node’s policy applies to everything under it. The rules for how that works are short, and the asymmetry between allowances and prohibitions is the part worth reading twice.

Allowances accumulate down, and can be reset

Section titled “Allowances accumulate down, and can be reset”

An imports.allow list is inherited: a node’s effective allowlist is its own entries plus every ancestor’s. external behaves the same way, and omitting it inherits rather than clearing.

A node’s allowlist compiles to a rule scoped to that node’s whole subtree — not to its immediate files. A folder three levels down with no imports of its own is still covered by the ancestor that stated one. Descendants that state their own allowlist are excluded from the ancestor’s rule, transitively, so exactly one allowlist rule governs any given file: the nearest one above it.

That is the mechanism behind the tightening you actually want, where each tier can only narrow:

"~/server/src/":
imports:
external: [effect]
allow: ["~/server/src/**", "~/contracts/**"]
children:
"modules/{module}/":
children:
"domain/":
# Narrower: the ancestor's allowances no longer apply.
imports:
reset: true
external: [effect]
allow: ["@/modules/{module}/domain/**"]

Neither is merged down the tree the way an allowlist is. Stated on a folder, a members or surface rule covers the subtree — every file under the folder is selected by it — and a descendant stating its own adds a second rule rather than replacing the first. Both fire. A vocabulary is a statement about every file in a tier, and a narrower one below it is a second statement, not an exemption.

A naming convention is inherited by the whole subtree, and a descendant that states its own replaces it. There is no reset for naming, because a convention is a single value rather than a list: restating it is the reset.

"~/database/src/":
name: { regex: "^(?:Database|[a-z0-9]+(?:-[a-z0-9]+)*)$" }
children:
"migrations/": { name: snake_case } # narrower, and stated
"**/": { name: kebab-case }

A deny is emitted once, over the subtree of the node that declares it, and descendants neither re-emit it nor can escape it.

This is why reset: true drops inherited allowances only. If reset also dropped prohibitions, a nested node could make its own subtree quieter than its ancestors — and the direction a mistake is dangerous in is the quiet one. As written, no arrangement of nodes can produce a subtree that refuses less than the tier above it.

The corollary: an exemption to a prohibition is declared by the prohibition. deny.except and deny.matchNot are the only ways out, and both are written by the author of the rule, in the same breath, where a reader of the rule sees them. A tier cannot opt itself out of a constraint someone else wrote about it.

Within one node, a prohibition wins. A target that a deny matches is reported with that deny’s message even if the allowlist would also have admitted it — which is the whole reason to write one, since the allowlist alone would already refuse anything outside it. The deny exists to say why this particular target is wrong, in a sentence the general allowlist message cannot.

Each is a distinct statement, each greppable, and none of them is silent.

Switch Says Leaves in force
reset: true this tier states its own allowlist from scratch all inherited prohibitions
unrestricted: true this tier has no allowlist yet all inherited prohibitions
layout: open this folder does not enumerate its file names its imports policy, its subfolders

unrestricted is required whenever a node states imports without an allow. You cannot leave an allowlist off by accident; the manifest will not decode. That turns an untightened tier into a sentence someone wrote and a reviewer saw, and makes grep -c unrestricted architecture.yaml the adoption backlog.

partial says “do not enumerate this folder’s own file list”. The node compiles no layout rule, so its direct children are not checked against its children keys — but everything else it states still applies to its whole subtree, and its child nodes still compile their own rules normally:

"src/":
partial: true
imports: { external: [effect], allow: ["src/**"] } # ← still in force
children:
"deep/":
message: "deep/ admits only *.ok.ts"
children: { "*.ok.ts": {} } # ← still enforced

It is the incremental-adoption switch: bring one branch of a large repository under policy without first having to describe every file sitting beside it.