Skip to main content

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.

SituationCommand
I have a codebase and want today's structure written downarchitecture init
I want to adopt a known architecturearchitecture init --template fsd
I don't know which architecture this repo followsarchitecture 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.

FormFieldReads as
candepends_on"api may import core"
cannotcannot_depend_on"api may never import legacy"
can onlyconsumed_by (on the target)"db is reached only through repository"
mustmust_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.

Contradictions fail at parse time

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 = true isolates a component's direct subdirectories from each other — the rule behind Feature-Sliced Design and bulletproof-react, with FSD's @x public-API escape hatch honored.
  • deny_capabilities forbids a component the ambient powers that break purity: network, filesystem, storage, dom, process, time, random. A Date.now() in a pure domain comes back with its file and line.
  • max_cyclomatic / max_cognitive put 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

FieldMeaning
nameComponent identity, referenced by every other component's rules
pathsPath globs claiming files, repo-relative with /. * stays in one segment, ** crosses
depends_onThe only components this one may import
cannot_depend_onComponents this one must never import
consumed_byThe only components allowed to import this one. [] admits nobody
must_depend_onComponents every importing file of this one must reach
interfacePublic entry files; others may import only these
external_denyExternal specifier patterns this component must not import
slicesIsolate direct subdirectories from each other
deny_capabilitiesAmbient capabilities this component must not use
max_cyclomatic / max_cognitivePer-function complexity budget
min_coverageLine-coverage floor, a fraction in (0, 1]
roleFree-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

KeyValuesDefaultEffect
schema1requiredContract schema version; a mismatched build fails loudly
modeoff, warn, new-violations, strictnew-violationsHow violations are enforced
unassignedignore, warn, forbidwarnWhat to do with files no component claims
couplingoff, low, medium, highlowHow 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.