The architecture contract
Most analysis tools stop at reading the code. The contract goes one step further: you write down which parts of your codebase are allowed to depend on which, in one small file, and every build checks the real code against it.
.ovecc/architecture.toml is that file. Each component claims files by path glob;
depends_on is the allow-list of what it may import. Everything else is a verdict.
# .ovecc/architecture.toml
schema = 1
[[component]]
name = "api"
paths = ["src/api/**"]
depends_on = ["core"] # the api may use core, nothing else
[[component]]
name = "features"
paths = ["src/features/**"]
depends_on = ["core"]
slices = true # and features may not import each other
[[component]]
name = "core"
paths = ["src/core/**"]
deny_capabilities = ["network"] # pure domain: no fetch, no I/O
max_cyclomatic = 8 # keep core functions simple
Run the check and every breach comes back with a file and a line:
$ ovecc architecture check
Divergences (1):
[High] api -> features is not in the contract
src/api/routes.ts:2 (../features/billing/service)
Slice isolation breaches (1):
[High] features/billing -> features/users breaks slice isolation
src/features/billing/service.ts:1 (../users/repo)
Denied capabilities used (1):
[Medium] core uses denied capability 'network'
src/core/pricing.ts:3 (fetch)
Complexity budgets exceeded (1):
[Medium] core: 1 function over the cyclomatic budget
src/core/pricing.ts:8 (cyclomatic 11 > 8)
Four kinds of decay in one run: a layer reaching where it shouldn't, a feature tangling into its neighbor, a network call inside code you promised was pure, and a function creeping past the budget you set.
Getting a contract
You do not write it from scratch.
| Situation | Command |
|---|---|
| I have a codebase and want today's structure written down | architecture init |
| I want to adopt a known architecture | architecture init --template fsd |
| I don't know which architecture this repo follows | architecture suggest |
init drafts the contract from the graph you already have, so every entry mirrors a real
import and day one has zero violations. Governance from then on is deleting the
entries you regret.
The four constraint forms
depends_on is an allow-list, and an allow-list is strictly positive: it can say what is
permitted and nothing else. Terra & Valente's DCL identifies four constraint forms an
architecture actually needs, and three of them cannot be written as a permission.
| Form | Field | Reads as |
|---|---|---|
can | depends_on | "api may import core" |
cannot | cannot_depend_on | "api may never import legacy" |
can only | consumed_by (on the target) | "db is reached only through repository" |
must | must_depend_on | "every route handler imports auth" |
[[component]]
name = "api"
paths = ["src/api/**"]
depends_on = ["repository"]
must_depend_on = ["auth"]
cannot_depend_on = ["legacy"]
[[component]]
name = "db"
paths = ["src/db/**"]
consumed_by = ["repository"]
[[component]]
name = "legacy"
paths = ["src/legacy/**"]
consumed_by = []
consumed_by is declared on the target, because that is where the sentence lives:
"the database is reached only through the repository" is one claim about the database,
not an edit to every other component's allow-list — and the allow-list version silently
breaks the moment somebody adds a component. consumed_by = [] admits nobody, which is
the strangler-fig rule: no new code may touch this module, without naming a single
consumer.
must_depend_on is judged per file. A file that imports nothing at all is exempt: it is
a leaf (constants, types, a stylesheet), not a route handler that forgot its auth. A
required dependency implies permission, so it is never also a divergence.
A component that both forbids and requires the same target, or one that declares a
dependency the target's consumed_by does not admit, is a contract error, not a
finding. It fails when the file is read, so every import carries exactly one verdict
and no reader is ever asked which half of the contract wins.
Because consumed_by judges only the files a component claims, a repository leaning on
it should also set unassigned = "forbid", so a file outside every component cannot
reach a closed one unnoticed.
Beyond the import graph
Three checks read past the imports (JavaScript/TypeScript):
slices = trueisolates a component's direct subdirectories from each other — the rule behind Feature-Sliced Design and bulletproof-react, with FSD's@xpublic-API escape hatch honored.deny_capabilitiesforbids a component the ambient powers that break purity:network,filesystem,storage,dom,process,time,random. ADate.now()in a pure domain comes back with its file and line.max_cyclomatic/max_cognitiveput a per-function complexity budget in the contract, so "keep the core simple" becomes a rule the build can check.
min_coverage is a fourth fitness function, and the only one that reads a file your
build produces rather than a metric ovecc derives: a fraction in (0, 1], checked only
when a coverage tracefile was indexed. With no tracefile the component is unmeasured,
not at 0% — calling it 0% would say more than the data does.
Virtual interfaces
Interfaces are virtual: list a component's public entry files in interface and ovecc
enforces them against the real imports. You get encapsulation without barrel files or an
extra re-export layer, and an import that reaches past them is an
architecture/interface-bypass.
[[component]]
name = "billing"
paths = ["src/billing/**"]
interface = ["src/billing/index.ts"]
Component fields
| Field | Meaning |
|---|---|
name | Component identity, referenced by every other component's rules |
paths | Path globs claiming files, repo-relative with /. * stays in one segment, ** crosses |
depends_on | The only components this one may import |
cannot_depend_on | Components this one must never import |
consumed_by | The only components allowed to import this one. [] admits nobody |
must_depend_on | Components every importing file of this one must reach |
interface | Public entry files; others may import only these |
external_deny | External specifier patterns this component must not import |
slices | Isolate direct subdirectories from each other |
deny_capabilities | Ambient capabilities this component must not use |
max_cyclomatic / max_cognitive | Per-function complexity budget |
min_coverage | Line-coverage floor, a fraction in (0, 1] |
role | Free-form layer identity for templates ("fsd/shared"); no check reads it |
A depends_on entry can also be a table, when the dependency is tolerated but on its way
out:
depends_on = [{ component = "legacy", deprecated = true }]
Top-level policy
| Key | Values | Default | Effect |
|---|---|---|---|
schema | 1 | required | Contract schema version; a mismatched build fails loudly |
mode | off, warn, new-violations, strict | new-violations | How violations are enforced |
unassigned | ignore, warn, forbid | warn | What to do with files no component claims |
coupling | off, low, medium, high | low | How loudly behavioral coupling is reported |
warn caps every verdict at Low, so nothing gates — the mode to start in. new-violations
gates on everything except what the baseline holds. strict ignores the baseline
entirely, so the whole debt gates again.
Adoption
Adoption is meant to be gradual.
check --freeze records today's
violations in a per-component baseline (one line each, so branches merge cleanly), gates
only new ones from then on, and drops entries as you fix them so the count never climbs.
For agents
Agents can read the contract before editing, through
architecture show <path> or the ovecc_architecture
MCP tool. It answers the pre-edit question — "I'm editing this
file, what am I allowed to import?" — from the contract alone, with no index required.
Related
- Rules reference — every
architecture/*verdict and its severity - Governance guide — the simpler, language-neutral rules in
config.toml