Skip to content

A gate's verdict is evidence, and a rule the repository enforces itself is proven by it

A gate's verdict is evidence, and a rule the repository enforces itself is proven by it

majordomus://adr/adr-0061proposed governanceevidence

61. A gate's verdict is evidence, and a rule the repository enforces itself is proven by it

Context

On 2026-09-14 a supervised repository, catharsis-as-a-service, added a blocking rule of its own: project.no-scroll-containers — nothing on the published site carries a scrollbar of its own. It is enforced, and by machinery of exactly the kind this tool asks for. scripts/validate-layout.py reads the stylesheets and the templates and exits 1 on a scrolling declaration, a scrolling utility class or a minimum width inside a figure or table selector. It is a step of scripts/validate.sh, which is that repository's gate and runs in its CI. Five unit tests prove the validator fails on each defect it exists to catch. A browser test walks every element of seven pages at three widths in two languages.

majordomus doctor, in that tree, said:

blocking without a validator — 7 of 48 blocking rule(s) carry no x-majordomus block, so no validator runs for them and the doctrine verdict below says nothing about them

The verdict is correct and the vocabulary is the defect. ADR 0048 gave a rule two declarable shapes and a third state for the rest: dispatched names a validator that lib/doctrine.sh calls, gated names tests, and everything else is reviewed. The join behind them is already good: rules/ does not re-implement evidence, it binds the tests a rule names to the executions recorded against them and reads ADR 0041's ProofState; gate coverage is derived from .ai/repo/ci/gates.yaml "so that a rule's gate coverage is derived from the planner's own input rather than restated here"; and a case reaches a gate "without either naming the other".

It reaches them through constants. CASES_DIR = "test/cases/", SUITE_RUNNER = "test/run.sh", CRATE_TESTS_DIR, CRATE_RUNNER — the derivation knows this repository's layout, because until now the only rules that mattered were this repository's own. A supervised repository's tests are at tests/python/test_validators.py, its enforcement is python3 scripts/validate-layout.py, its gate is scripts/validate.sh, and none of those can be named: validator means a function of this tool, enforced_by means a command of this tool, tests resolves only under paths this tool ships. The rule that stops a scrollbar reaching production is machine-enforced, and the only thing Majordomus can say about it is that somebody reads it.

ADR 0048 saw the shape of this and deferred one half of it deliberately: "Record a verdict for gates, so gated could reach proven. Tempting, and rejected for now … teaching it to record an arbitrary gate's exit code is a larger change than this decision needs." Since then the larger change has arrived in pieces — ADR 0041's execution record, the CI model the planner reads, and gates/, which already judges what each gate said and whether that verdict still describes the tree. Nothing joins them to a rule.

Decision

A rule may name the enforcement its own repository wrote. The enforcement block gains command: the executable that refuses the rule, as the repository runs it (python3 scripts/validate-layout.py), with the exit code that means refusal. It is neither of the existing modes renamed — a validator is dispatched by this tool, a command is dispatched by the repository's gate — and the derivation order of ADR 0048 is unchanged: a validator still wins, then tests, then a command, then a reason.

What makes a test a test is data, not a constant. The directories and runners that let a case reach a gate move out of the Rust and into the repository's layer, where CASES_DIR and SUITE_RUNNER become this repository's own values of a declaration every repository writes. Nothing changes here; everywhere else becomes expressible.

The gate's verdict is an execution, and the ledger is the one that exists. A gate run records what ADR 0041 requires of any run — outcome, duration, commit, tree state, source digest, origin, and the command that reproduces it — under an identity derived from the gate id, as a test's is derived from its path. gates/ already reads verdicts; this is what it means to keep one.

A rule is proven by a verdict at the commit that carries the rule. ADR 0041's states apply unchanged: a verdict from an earlier commit is stale, a verdict against a dirty tree proves no commit, a gate that never ran is not_run. proven keeps one meaning across claims, tests and rules, which is the reason for reusing the model instead of adding a fourth one.

A declaration that does not resolve is a defect of the rule. A command whose executable is not in the tree, or a test path outside every declared test root, fails the loader the way a category without a validator already does. Absence of any declaration is still reviewed, still counted apart, still never counted as passing.

Alternatives rejected

Write a Majordomus validator for each supervised repository's rule. It does not scale and it is the wrong boundary: one repository's rule is about stylesheets, the next one's is about SQL migrations. The repository had already written the validator; what was missing was the join.

Let the author write reviewed_because. That is what the tree does today, and it files five unit tests, a CI step and a browser test under "a person's opinion". A mode that absorbs everything reports nothing.

Let the gate declare which rules it proves. Enforcement would then live in two files that must agree, and the rule — the document a reader opens — would stop saying how it is held up. ADR 0048's principle holds: the block on the rule is the declaration.

Record every gate's verdict. Only a gate some rule's proof depends on needs one, and only at the commit. Recording all of them turns the ledger into the build log ADR 0041 refused.

Consequences

The rule model stops being about this repository. A supervised repository that declares its test roots and names its own enforcement gets the same proof graph, the same states and the same ratchet as this one — which is the difference between a supervisory layer and a self-supervising program.

CI needs a way in: a gate must be able to record an execution with origin: ci from inside a workflow. That is a capability and a credential question this decision opens and does not answer.

rule-proof-check ratchets: rules leaving review_only change the baseline, and a repository that names a gate it never runs is told so instead of being counted apart in silence.

Two constants become schema, which is a migration for this repository and an enablement for every other.

Where this comes from

canonical file
.ai/repo/adrs/0061-a-gate-s-verdict-is-evidence-and-a-rule-the-repository-enfor.md
identity
adr-0061
also served as
MCP resource majordomus://adr/adr-0061 · majordomus entity show adr/adr-0061 · GET /api/v1/entity?uri=majordomus://adr/adr-0061
in the Cockpit
/cockpit/objects/adr/adr-0061

This page is generated from the file above by scripts/generate-site-data; editing the page changes nothing, editing the file changes every surface. The relations above are edges of docs/generated/graph.json, which the executable writes from the same index.

Searches the titles and descriptions of every page this site publishes.