Skip to content

Graph

Every other family judges one file, or one edge leaving it. graph judges the whole repository resolved at once: whether two files import each other, whether anything imports a file at all, and whether one tier can reach another through any number of intermediate hops. No single file can answer those, and a rule about one edge cannot: a barrel hop between an adapter and a live implementation satisfies every per-edge allowlist and still puts the adapter one import away from the thing it was never supposed to touch.

graph:
cycles:
- name: no-cycles
message: "These files import each other, directly or through others."
within: "packages/*/src/**"
withinNot: "**/*.test.ts"
orphans:
- name: no-dead-modules
message: "Nothing imports this file. Register it, or delete it."
within: "packages/*/src/**"
withinNot: "**/*.test.ts"
entry: ["packages/*/src/index.ts", "packages/*/src/main.ts"]
reach:
- name: adapters-through-ports
message: "An adapter reaches infrastructure only through a port."
from: "packages/*/src/adapters/**"
to: "packages/*/src/infrastructure/**"
via: "packages/*/src/ports/**"

graph is a top-level section, not a node: its rules are statements about the repository, and every glob in it is expanded through aliases like the rest of the manifest.

A strongly connected component — two or more files that can each reach the other, or a file that imports itself — among the files within (and not withinNot). Each component is reported once, and the violation’s subject is the component as a sorted set, so its baseline entry survives an edge added or removed elsewhere in the same cycle.

A cycle that runs through a file outside the scope is not seen: withinNot: "**/*.test.ts" means a test importing the module it tests, which imports a helper the test also imports, is not a cycle the rule speaks to.

A file within scope that nothing imports and that is not an entry. An importer counts from anywhere — a fake imported only by a test is not an orphan, because the test imports it. The files that are imported by nothing by design are the program’s entry points, and entry is where a policy lists them.

A file matching from that can reach a file matching to through any number of hops without passing through a via file. via is the tier that was supposed to mediate: a path that steps onto one is fine, and only a path that avoids every via is the violation. Omit via for a plain “must never reach, by any route”.

A violation is reported per origin and target, and the route that reached it is appended to the message:

packages/server/src/adapters/http/todos.ts
[adapters-through-ports] An adapter reaches infrastructure only through a port.
(route: packages/server/src/adapters/http/todos.ts → packages/server/src/shared/db.ts → packages/server/src/infrastructure/pg-live.ts)

The route is in the message and not the subject, so the fingerprint is (origin, target) and survives the route changing underneath it.

Every file the CLI walked, and for each the walked files it resolves to. An external package, a builtin, a file outside the walked roots, and an edge nobody can resolve are not in it — the first three are not the repository’s shape, and the last is reported by check on its own. A type-only import is an edge like any other: it is still a dependency between the two files.

Graph rules are evaluated by architecture check and not by the oxlint plugin. The plugin sees one file at a time, with no view of the others, and cannot answer “does anything import this?”. Both adapters compile and probe the rules at load — a via that covers its own target fails oxlint too — but only the CLI has the graph to evaluate them against. See the CLI for what that means for how the two are run.