87. Evidence is indexed by subject and judged at the presented revision
Context
ADR 0041 made a claim's proof a recorded execution. The join it built is by path: a claim in
docs/CLAIMS.yaml names a test file, TestId turns that path into an identity
(suite:<case>, crate:<binary>), and the tracked ledger .ai/repo/evidence/ledger.json
keeps the latest execution of each identity and nothing older. majordomus evidence show
judges every claim against that ledger at the working tree. How many claims reach each state
is what majordomus evidence show --format json | jq .totals prints; this record copies none
of it.
That join answers one question about one kind of subject, and every other surface that wants to say "verified" has no way to ask it:
- Nothing ties a site widget to a subject. A homepage panel, a feature page, a command's example and an MCP tool's page describe behaviour, and none of them names the claim, rule or test that would prove it. Where a page shows proof at all, the word was placed by hand.
- The site cannot ask whether a claim is proven at the commit it shows.
evidence showjudges the working tree of whoever runs it. The site is built from one commit, whichsite/data/build.jsonnames, and no command judges that commit by the ledger committed in it. - CI throws its runs away. The suite and the crate tests run on every merge to master and
print exactly what
evidence recordconsumes, and nothing of those runs reaches the ledger (ADR 0080's context measures this). - A skip reads
failing.Outcome::proves()holds for a pass only, and both judgements map every execution that did not prove tofailing, so a test that declined to run is reported as counter-evidence. In the other direction, the suite runner writesokfor a case that declines by exiting zero, which records it as a pass; PR #629 corrects that (ADR 0080, decision 5). - The rules module holds a second copy of the freshness derivation.
rules/mod.rshas its ownchanged_sinceand its own mapping from an execution to a state, beside the one inevidence/mod.rs. Both read the same ledger and must agree on what a commit's diff is, and only review holds them together. - The entry preflight proposed in PR #578 adds another. Its
verification.testscheck counts a recorded run as current by a filter of its own: a clean recorded tree, a diff from the run's commit that touches only the ledger, and a matching digest. It has no containment test, so a run recorded on a commit that HEAD does not contain can pass it. Itsverification.enforcementcheck recountsrules::reportinstead of reading the corpus verdict. - ADR 0068 and ADR 0080 disagree about where CI evidence goes. ADR 0068, "CI evidence is kept where the run happened, and published with the commit it proves" (proposed, PR #577), keeps a run's evidence as the run's artifact, publishes it to the site as a dataset, and says that CI's evidence is not committed into the tracked ledger. ADR 0080, "A CI run reaches the tracked ledger through a recording branch, never through the run that produced it" (proposed, PR #629), commits a trunk run's executions into the tracked ledger through a recording pull request. ADR 0080 does not cite ADR 0068, and neither says which one a surface reads when they differ.
- As PR #577 is written, a run record is published as a verdict. Its collector records the
run's executions first and derives the report after, so the transcript the site publishes as
currentis a verdict over the run's own rows, recorded from a checkout nobody measured.
Another proposal, ADR 0071, "A feature exists only when its delivery is computed and every dimension passes" (proposed, PR #600), declares delivery dimensions that read test evidence and holds its own implementation of whether one commit contains another.
This record decides the model every one of those surfaces reads, before any of it is built, so that each slice of the work is held to one definition rather than to its own.
Decision
Evidence is indexed by subject (a claim, a rule, a feature, a command, a capability, an MCP tool, a use case), and every surface judges a subject at the revision it presents, through one authority, one freshness function and one aggregation. E is the commit an execution was recorded on, T the tree its run measured, P the presented revision and Q the presented tree.
1. One authority
A verdict (ProofState) is derived only from the tracked ledger
.ai/repo/evidence/ledger.json. This adopts ADR 0080. Two things feed the ledger:
- the recording branch: a trunk run, after a merge and on a schedule, records its
executions with
evidence record --origin ciagainst the commit it ran and opens a pull request that changes only the ledger, from a branch named for that commit. At most one such pull request is open; a newer recording replaces it. It passes the same required checks as any other pull request; - deliberate, committed local recordings.
The recording stops at a covered commit. A trunk commit C is covered when the ledger
committed in C already holds what a recording of C would add: E, the commit that the newest
ci execution in that ledger names (by its recorded time), is contained in C, and changed'
from E to C is empty. A trunk run of a covered commit is not recorded, whether a merge or the
schedule started it: the recording job ends before it runs a test. Decision 4 judges every
execution in the ledger the same at C as at E, because the ledger path is outside changed',
so a diff that touches only the ledger still proves. Recording C would change nothing but the
commits the ledger names, and that change would open the next recording pull request, whose
merge would start the next run, without end, each round costing one full suite run. Landing a
recording pull request on the commit it measured makes a covered commit, however it is merged:
its diff from E is the ledger alone. A recording job that cannot compare E with C fails and
says why; it neither records nor passes C as covered.
The stop reads what the ledger covers, never the newest commit's own diff, because a
ledger-only commit is not always covered. When the recording of a merge M1 lands on top of a
later merge M2 whose own recording never opened or never landed, the landing commit's diff from
its first parent is the ledger alone, but changed' from M1 to it holds M2's changes. It is
not covered, so it is recorded: by the run its own landing starts, or, if that run is lost too,
by the schedule, which is what the schedule exists for on a quiet trunk (ADR 0080, decision 1).
A ledger-only commit on top of a covered one, such as a deliberate local recording, stays
covered.
This amends ADR 0068 sentence by sentence. The paragraphs of its Decision section, as written on PR #577's branch, are in order the artifact (¶1), the run reference (¶2), the publication (¶3), the dispatch refresh (¶4) and the offline run link (¶5).
- ¶1, "It records the executions through
majordomus evidence record --origin ciand derives the report throughmajordomus evidence show." Amended: the collector derives the report first, withevidence showover the tracked ledger as committed at the run's commit, and only then records the run's executions. The report is the tracked ledger's verdict at that commit. - ¶1, "The tracked ledger stays what ADR 41 made it, the evidence of whoever records into the tree. CI's evidence is not committed into it." Superseded by ADR 0080: a trunk run's executions reach the tracked ledger through its recording pull request, and the validating run never commits them itself.
- ¶3, "Evidence of the published commit is
current." Amended:currenttakes this record's definition (decision 3). Until the site publication is typed,scripts/pages evidencepublishescurrentonly when the run is of the published commit, every job that ran tests measured a clean tree, the report was derived on a clean checkout of that commit, and nothing was absent, refused or failed. Otherwise it publishesstale(an ancestor's run, or a run that recorded a failure) orunknown(the run cannot confirm the report), naming each reason. The rule can only withholdcurrent; it never changes the report. - ¶4, "That publication finds evidence of its own commit and says
current." Amended the same way: the dispatched publication sayscurrentonly when that definition holds. - Amended when the interim section retires. These sentences hold for the interim "Recorded
by CI" section and change when the publication slice (I1959) makes
scripts/pagescallevidence publish. From then on, the site's publication carries the first matching row of decision 3's publication table:- ¶3, "
scripts/pages evidenceruns before the build, finds theevidenceartifact of master's validation recorded against the commit being published or its nearest ancestor, and writessite/data/evidence.json." Amended: the script callsevidence publishin the build's checkout, which reads that run record from the artifact or from its gh-pages copy underevidence/<sha>/. - ¶3, "Evidence of an ancestor is published as stale, naming the commits and the changed
files between." Amended: a record of an ancestor is listed by its commit, with the
commits and the changed files between. It can cap the verdicts it contradicts (row 2), and
it never makes the publication
current: without P's own record, the table's first matching row isdirty,stale,pendingorunknown. - ¶3, "No evidence at all is published as unavailable, with the reason." Amended: no
evidence at all is
pendingwhile P's CI run has not concluded andunknownafter that, naming the reason.unavailableis not a publication state, and it retires with the section. - ¶3, "None of the three blocks publishing." Amended: no publication state blocks publishing. The site build refuses only a dataset that is not bound to it (I1959).
- ¶4, "If master has moved on, nothing is dispatched: the newer commit's publication already shows this evidence as stale, and the newer commit's own run will refresh it." Amended: the newer commit's publication lists this evidence as an ancestor's record (decision 3), and the newer commit's own run refreshes it.
- ¶3, "
- Standing, by sentence:
- in ¶1, "CI evidence is an artifact of the run that produced it."; the sentence naming the
evidencejob and the reports it gives the collector; the coverage summary and the manifest; and the ninety-day retention; - ¶2 whole;
- in ¶3: the nearest evidence, and saying how near it is; the dataset as an uncommitted
input of one deployment; and the one thing ruled out. Until the interim section retires,
every other ¶3 sentence but the
currentone stands too; - in ¶4: the refresh as a dispatch, not a chain; the dispatch of
pages.ymlwhile the run's commit is still master's tip; andpages.ymltriggered by the push alone, a dispatched run waiting for a running publication. Until the interim section retires, the moved-on sentence stands too; - ¶5 whole.
- in ¶1, "CI evidence is an artifact of the run that produced it."; the sentence naming the
What a run's executions are, stated once: a validating run's executions decide no verdict in that run's artifact or on the site, where they are a run record (counted, linked and kept). A trunk run's executions become verdicts only when its recording pull request lands them in the tracked ledger (ADR 0080).
2. Supplementary records never strengthen a verdict
- Run records are written once per
evidence recordcall and kept in the CI artifact, on gh-pages underevidence/<sha>/, and locally under.ai/local/evidence/runs/, bounded at the newest 50. - Coverage records are the other kind, carried inside a run record. Both kinds carry their own commit and tree.
- ADR 0041 rejected a full execution history in favour of the latest execution per test. That is amended only outside the tracked ledger: the ledger still keeps the latest execution per test, and history lives only in run records.
- Local rows are written only with
evidence record --ledger local, to.ai/local/evidence/ledger.json, and are advisory: a surface shows them with their origin and decides nothing from them. evidence recordkeeps writing the tracked ledger by default, as case 124 holds.
The monotone rule. Take a supplementary record of the same test, measured on a clean tree at
a commit R that contains E and is contained in the presented revision. If its outcome is fail,
timeout or error, it caps the verdict at stale, with a detail naming the record (its origin,
its identity and R). A supplementary record withholds proven; it never decides a verdict,
which is why the cap is stale and not failing.
The rule has two sources:
- the working copy of the tracked ledger when a commit is presented (decision 5): every execution it holds that the ledger committed in the presented commit does not;
- run records.
So no surface shows a verdict stronger than the authority it reads for the revision it presents,
a clean failing run it can see withholds proven, and every difference names its record.
evidence show and evidence show --presented HEAD judge different revisions (the working
tree, and HEAD as committed): the first can read failing where the second reads stale, and
neither reads verified.
3. The read rule per surface
| Surface | Verdict read from | Presented revision | Supplementary records |
|---|---|---|---|
evidence show; evidence.report over the CLI, HTTP and MCP | the tracked ledger in the working tree; with --presented, the ledger as committed in the presented commit | the working tree (HEAD plus tracked, staged and untracked changes); with --presented <rev>, the checked-out commit (decision 5) | working tree: none, since the working ledger is the authority. --presented: the working ledger's executions that the committed ledger lacks, through the monotone rule, listed beside the verdict |
evidence claim, evidence proves, rules report, rules show, rules proves, including the rules corpus verdict (RulesVerdict, proposed in PR #597) | the tracked ledger | the working tree | not read. The corpus verdict is proven only when every blocking rule is proven (decision 4) |
evidence subject | the tracked ledger, through the subject index (decision 9) | the working tree, or --presented | the local run index, and a CI artifact directory given by --from: listed beside the verdict, applied only through the monotone rule |
evidence coverage, evidence runs, evidence run | none: they show supplementary records only | as above; coverage is current only as decision 12 says | the local run index; --from |
The gates: scripts/evidence-check, evidence show --check | the tracked ledger | the checked-out tree | never |
| The Cockpit | the tracked ledger, through the subject index | the working tree | a separate, advisory local-run column, through the monotone rule |
The site: evidence publish, run in the build's checkout of P | the ledger committed in P | build.json.commit, capped by build.json.dirty | P's CI run record if there is one, else the nearest ancestor's, marked with its commit; through the monotone rule |
The site's interim "Recorded by CI" section: scripts/pages evidence, ADR 0068 as amended by decision 1. Retired when the publication slice (I1959) makes scripts/pages call evidence publish and the routes slice (I1958) folds the section into the runs section | the tracked ledger as committed at the run's commit, derived by the CI evidence job before it records the run's executions | the published commit: current needs the run's commit to equal it, and an ancestor's run is stale | the run's manifest (totals, the producers' measured trees, the report's tree, what was absent or refused): it can only withhold current, never change the transcript |
The entry preflight (majordomus env preflight, PR #578): its verification.tests and verification.enforcement checks, and what renders them (the entry briefing, the Cockpit overview, /api/v1/environment/preflight and the MCP tool majordomus_preflight). The one known exception to decisions 4 and 8 | the tracked ledger at the working tree. The tests check counts a run as current by its own filter, which has no containment test; the enforcement check recounts rules::report instead of reading the corpus verdict | the working tree | not read. The follow-up slice (I1965) moves the tests check onto the freshness function and the enforcement check onto the corpus verdict |
The delivery report's required_tests_current and test_evidence_published dimensions (PR #600, ADR 0071) | pending there today. When computed: the first passes only when the feature's subject verdict at the trunk revision is proven, which a busy trunk does not reach (Consequences; the owner decides before I1960 lands whether the process makes it reachable or the dimension binds to a reachable state); the second reads the site's publication, which carries the tracked ledger's verdict at the published commit (decision 1) | the trunk revision; the published commit | through the publication only. Neither reads a CI run's own rows, and neither has a reader of its own |
evidence record (the writer) | writes the tracked ledger by default; --ledger local writes .ai/local/evidence/ledger.json | stamps the E and T its run measured (decision 6); until that lands, HEAD and the recorder's own tree | writes a run record for the call; with --ledger local it also appends to the local run index |
Publication states are site-level words, not proof states, and each is already filed as a
status token of the design system. They govern the site's publication through evidence publish. The interim section keeps ADR 0068's words as decision 1 amends them, among them
unavailable, which is not a publication state and retires with the section. A publication
carries exactly one state, decided at publication time from what it can observe: build.json,
the run records it read and the conclusion of P's CI run. The first matching row wins.
| # | Observed at publication | State |
|---|---|---|
| 1 | build.json.dirty is not false | dirty |
| 2 | a run record read, P's own or an ancestor's, caps at least one verdict shown through the monotone rule | stale |
| 3 | P's own run record is read, and every tree it carries was measured clean | current |
| 4 | P's own run record is not read, and P's CI run has not concluded | pending |
| 5 | anything else: P's run concluded without a run record (its evidence job skipped, cancelled or failed), P's record carries a dirty or unknown tree, the record is unreachable (the artifact expired and no gh-pages copy exists), or no CI run of P is found | unknown, naming which |
- A state and its causes are separate: whatever row matched, every record that capped a
verdict is listed beside it, so a dirty build that a record also contradicts reads
dirtyand still names that record. - A record of an ancestor only can cap a verdict (row 2) and nothing else. It never makes a
publication
current: without P's own record the state isdirty,stalewhen the record capped a verdict, and otherwisependingorunknown, with the ancestor's record listed by its commit, the commits between and the changed files. The interim rule of decision 1 publishes an ancestor's run asstaleonly because there the transcript itself is the ancestor's; this table replaces that rule when the interim section retires (decision 1). pendingis the one state a later publication of the same P is expected to replace, when P's run concludes (ADR 0068's dispatch refresh, ¶4).
4. Freshness is one function
changed' is the set of paths changed from E to the presented side, minus the ledger path. The
first matching row wins.
| # | Condition | State |
|---|---|---|
| 1 | no test | no_test |
| 2 | no runner drives the path | unrunnable |
| 3 | nothing recorded | not_run |
| 4 | skip | not_run, with the reason |
| 5 | fail, timeout or error | failing |
| 6 | pass; git cannot compare | stale |
| 7 | pass; E not contained | stale |
| 8 | pass; contained; the test's digest moved; no input changed | stale |
| 9 | pass; contained; a declared input changed | stale |
| 10 | pass; contained; changed' empty; recorded tree clean; presented tree clean | proven |
| 11 | as row 10, but the recorded tree is dirty or unknown | inputs_unchanged |
| 12 | as row 10, but the presented tree is dirty or unknown | inputs_unchanged |
| 13 | pass; contained; changed' non-empty; inputs known and none changed | inputs_unchanged |
| 14 | pass; contained; changed' non-empty; the route names no inputs | stale |
Only row 10 renders "verified", on every surface: a rendered word, a badge and the rules corpus
verdict alike. rules report says proven only when every blocking rule is proven, because
inputs unchanged is not proven (ADR 0041). A diff that touches only the ledger still proves;
that is what lets a recording pull request, a child of E, prove E. A skip is never proven and
never failing.
A claim's inputs are its source, its implementation and its test source; a rule's are its test
source and its definition; any other route's are its test source and the implementation paths
derived for it, or none. A rule is judged by this same function. The working-tree diff stays
evidence::changed_since, and the rules module's copy is removed.
The entry preflight of PR #578 is the one known exception, named in decision 3, until its follow-up slice lands: its tests check can call a run current when E is not contained in HEAD, and its enforcement check recounts the rules.
5. The presented revision
- By default the presented revision is the working tree.
--presented <rev>must name the checked-out commit. It is judged by the ledger committed in it. The presented tree is measured, ignoring the ledger, and a tree given on the command line can only weaken that measurement.- The working copy of the ledger is not the authority for a presented commit, and it is not
ignored either. Every execution it holds that the committed ledger does not is a
supplementary record, read through the monotone rule (decision 2). A clean failing run that
the checkout holds uncommitted therefore withholds
proven. - On the site the presented revision is
build.json.commit, andbuild.json.dirtycaps the verdict. - Judging any other commit is not built.
6. E and T are measured by the run
- The runner measures its commit and its tree at the end of the run, ignoring its own named outputs, and carries both beside its report.
- The recorder stamps them. It refuses a report whose commit is not HEAD, and records T as
unknownwhen there is no measurement. - A pull-request run's commit is GitHub's merge commit, with
head_shabeside it. Only a trunk run can prove a trunk commit. - A report states only what ran: a crate binary that ran no test, or a filtered subset, is a skip.
7. Containment is one implementation
Whether one commit contains another is decided once, in git/mod.rs, beside git::is_ancestor,
and shared with the delivery subsystem (ADR 0071). The delivery subsystem imports it once it is
on master and deletes its own copy. Containment that git cannot decide is row 6, never proof.
8. Aggregation is one function
One function combines parts, over each state enum's declared order:
failingif any part fails;- otherwise the weakest of the parts that can carry proof;
- otherwise the weakest of all parts.
No second ranking is written anywhere. The first step exists because the declared order places
failing above not_run, so the weakest of a failure and an absence would read as the
absence: a failure is never hidden behind one. Per-state totals always accompany a verdict.
A subject's verdict is computed over its members' own verdicts, never over their raw routes. A
rule member contributes through one declared RuleState → ProofState mapping:
RuleState | ProofState | Carries proof | Why |
|---|---|---|---|
proven, inputs_unchanged, stale, failing, not_run, unrunnable | the same word | yes (unrunnable only when the rule itself judged it) | identical meaning |
gated | not_run | yes | a gate refuses violations, but no recorded run proves the behaviour |
reviewed | no_test | no | the rule declares that nothing executable expresses it |
unproven | no_test | yes | a blocking rule that names nothing is a defect and must weaken its subject |
dangling | unrunnable | yes | a named proof that does not exist; the subject also gets a dangling_member finding |
9. The subject index is derived only, in Rust
The index is derived from existing declarations and no list of subjects is authored: claims,
rules, features, commands with their # majordomus-covers: and # majordomus-negative:
headers, capabilities, MCP names and use-case edges.
| Key | Resolution |
|---|---|
claim:<id> | the claim join of evidence show |
rule:<id> | the rule join of rules report |
feature:<id> | members: its claims, rules, commands and use cases |
command:<path> | the suite cases whose first # majordomus-covers: or # majordomus-negative: line names the command, and the crate's CLI examples for a command that has one |
capability:<id> | its command's routes |
mcp:<tool> | the capability the registry names for the tool |
use_case:<id> | its scenario run; members: the commands it runs, the rules it exercises and the claims it evidences, never its owning features |
majordomus generatewrites the index assite/data/registry/evidence-subjects.json, in the executable's site-data directory. It never writes intosite/data/generated/, which the shell site generator owns and replaces wholesale, so that no directory has two writers.- The index holds no ledger content and no commit, so
majordomus generate --checkstays stable. - The site only joins to it:
scripts/generate-site-datajoins a panel's route to a key, and a subject the index does not have fails generation with the executable's refusal. - It names the shell derivations it replaces or holds equal: the
testsofcommands.jsoninscripts/generate-site-data, the header reading oflib/commands.sh, andscripts/ci/command-furnished. A case holds them equal until each reads the artifact. Where they differ, the index's first-line rule is the one kept.
10. Subject findings are a separate list
feature_without_evidence, freshness_unknown and dangling_member are findings of a subject's
answer, never of the claim report, whose findings are what evidence show --check gates on. They
stay advisory until the gate slice (I1962) makes them refusing and rewrites
.ai/repo/evidence-baseline.txt in the same change.
11. The test category is derived from the test identity
suite:<case> is end-to-end; crate:<binary> from tests/<binary>.rs is integration; then
unit, doctest and scenario, the last being a use case's run. verification.requires is not
added to feature/v1: what a feature requires is what its members name.
12. Coverage is supporting evidence
Coverage never decides a verdict. It is current only for its own commit, with a clean tree, and
on the site only when the presented tree is clean as well. Otherwise it is shown with its commit
and as stale.
13. Status-word token keys are hyphenated
The verdict words are filed in the design tokens with hyphens (inputs-unchanged, not-run,
no-test), because the token name pattern refuses underscores. ProofState::token() is the one
mapping from the wire word to the token; no template rewrites a word.
14. The public projection is allow-listed
What the site publishes about evidence is a typed projection whose fields are named one by one; a field not on the list is not published. Text a run printed reaches it only through the one redactor (I1950), and the published artifact still passes the generator's refusal of machine paths and credential markers.
15. Surface decisions
- MCP exposes evidence through tool calls only, with no resource templates.
- The required
cicheck does not need theevidencejob: evidence is refused at publication and at finish, not at merge. - The llvm-cov HTML report is published for the latest deploy only.
- Use-case evidence is not stamped with a commit in the committed catalogue; it is joined to a revision only through the deploy-time publication.
- No new shell automation is added (ADR 0069).
16. The recording pull request
A pull request opened with the workflow's own GITHUB_TOKEN starts no pull_request workflow,
so it would never receive the required checks decision 1 requires of it. The recording pull
request is therefore opened by a token whose events start the required workflow: either the
recording job dispatches validate.yml on the recording branch, or it uses a GitHub App token.
The owner decides between them before the CI wiring slice (I1954) lands.
Consequences
- Until the recording branch runs, the tracked ledger holds only local rows and the site renders
mostly
not_run. That is the true state, and it is why the CI wiring precedes the badges. provenat a trunk revision needs a quiet trunk. Row 10 holds at a trunk commit only when nothing but ledger-only commits separates it from E. A recording costs one full suite run, whose wall time ADR 0080's consequences measure, and its pull request then passes the required checks, while ADR 0068's context describes a trunk that moves every few minutes. When a code merge lands in that window,changed'from E to the landing commit is not empty, and the landed recording moves claims fromnot_runtoinputs_unchanged(row 13) orstale(rows 9 and 14), not toproven. ADR 0080's expectation that a recording moves claims toproven, a rendered "verified" on the site, and the delivery dimensionrequired_tests_current(decision 3) all hold only after such a quiet window. The owner decides between two ways out before I1960 lands: a process that makes row 10 reachable (for example, a merge queue that holds code merges from a trunk run's commit until its recording lands), or bindingrequired_tests_currentand the milestone's desired state to a state that is reachable. This record does not choose.- A ledger-only commit landing on top of a merge that no landed recording covers is recorded (decision 1), so a lost recording never leaves a merge unrecorded while the trunk is quiet.
- ADR 0068 changes only in the sentences decision 1 names: some at once, and the others when the interim section retires (I1959). ADR 0041 changes only outside the tracked ledger: run records keep history, and the ledger still keeps one execution per test.
- Every rendered "verified" traces to row 10 of one function, and every weaker word that differs between surfaces names the record that weakened it.
- A skipped test reads
not_runwith its reason instead offailing, and a guaranteed claim whose test only skipped is still reported as unsupported. - The rules corpus verdict proposed in PR #597 reaches
provenless often: a corpus whose blocking rules are merelyinputs_unchangedreads unproven. - The delivery subsystem gives up its private containment when it rebases.
- The entry preflight stays an exception until I1965 lands. This record names it so that it is not mistaken for the rule.
- The subject index is one more generated artifact. It is declared like every other one, so its merge is resolved by the derived merge driver.
- The work is tracked as the milestone
test-evidence, one issue per slice, I1945 to I1965;majordomus planderives what may be worked on and in what order.
Alternatives rejected
- Hard-coded badges and links. A badge written into a template is a claim with no execution behind it, and it goes stale the moment the ledger changes.
- A browser fetch of GitHub Actions. The published page would depend on the network and on an
API's limits at read time, it would show a run's own rows as a verdict, and
scripts/ci/link-checkdecides links offline. - Per-surface catalogues of subjects. Each surface would drift from the others; the command to test join already exists more than once in shell (decision 9).
- A parallel freshness enum. A second vocabulary for the same states is a second derivation. The publication states are kept apart from the proof states for that reason.
- Commit equality, E = P, as the test for
proven. It is unreachable once the record is committed: the commit that lands a recording is a child of E, never E. The ledger-only diff gives the same guarantee and can be met, but only at a trunk commit that nothing but ledger-only commits separates from E (Consequences). - Three evidence stores without a read rule. The tracked ledger, the CI artifact and a local ledger would each answer differently, and a reader could not tell which one a surface showed.
- Recording from the validating run itself. ADR 0080 rejects it: a pull-request run has no business writing to the repository it is judging.
- Publishing a verdict computed over a CI run's own rows. That is ADR 0068 as first written:
the rows were judged against a checkout nobody measured, so the site could say
currentfor a tree the run did not vouch for.
Anti-goals
| Anti-goal | Mechanism |
|---|---|
| a badge verified for a revision it did not test | freshness rows 6, 7 and 10–12; the site-build refusal (I1959); the deploy check (I1961) |
| a recorder vouching for a tree it did not see | decision 6 |
| a run record published as a verdict | decision 1's amendment of ADR 0068 |
| a partial run recorded as a pass | the crate count rule (decision 6) |
| a skipped case recorded as a pass | the skip word lands before CI recording (Sequence) |
| a failure hidden behind an absence | decision 8 |
a corpus verdict proven over rules whose inputs are merely unchanged | decision 4 |
| a recording that records its own landing, without end | decision 1's stop at a covered commit |
| a merge left unrecorded behind a ledger-only commit | decision 1: the stop reads what the ledger covers, not the newest commit's own diff |
| a publication state that two rows could both claim, or that no row covers | decision 3's first-match publication table |
| a surface greener than the ledger | the monotone rule |
a named revision judged proven over a clean failing run the checkout holds uncommitted | the monotone rule over the working ledger's uncommitted executions (decisions 2 and 5) |
| a second derivation of "proven" | decisions 4 and 8; the entry preflight is the one named exception until its follow-up slice |
| a hard-coded count in a template | the evidence validator (I1962) and 28_no_hardcoded_values |
| a widget without a subject | the generation refusal (decision 9) |
| a second inventory of subjects | decision 9 |
| a secret published | decision 14 |
| evidence lost when artifacts expire | gh-pages evidence/<sha>/ |
Residual risk: a hand-edited ledger row, caught by review only.
Sequence
The work is cut into slices, one issue each under the milestone test-evidence:
| Slice | Issue | Branch | What it delivers |
|---|---|---|---|
| Z | I1945 | feature/evidence-subject-adr | this record, the milestone and the issues |
| T | I1946 | feature/verdicts-are-status-words | the verdict words filed as status tokens (decision 13) |
| D | I1947 | feature/every-feature-reaches-a-test | every use case owned by a feature, and the Cockpit and deployments features reaching a test |
| A1 | I1948 | feature/evidence-is-judged-at-the-presented-revision | the freshness function, the presented revision, containment and aggregation (decisions 4, 5, 7 and 8) |
| S577 | I1949 | fix/the-ci-evidence-is-recorded-clean | PR #577's report derived before recording, its producers' trees measured, and the interim current rule (decision 1) |
| R | I1950 | feature/evidence-output-is-redacted-once | the one redactor (decision 14) |
| A2 | I1951 | feature/evidence-runs-are-typed | run records, measured provenance, coverage records and the crate count rule (decisions 2, 6 and 12) |
| SI | I1952 | feature/evidence-subjects-are-derived | the subject index (decisions 9 to 11) |
| C | I1953 | feature/evidence-answers-by-subject | the subject, coverage and run commands |
| B | I1954 | feature/every-runner-reaches-the-ledger | the recording branch, its stop at a covered commit, and every runner's measured report (decisions 1 and 16) |
| P | I1955 | feature/evidence-publication-is-a-projection | the allow-listed publication and its first-match states (decisions 3 and 14) |
| E | I1956 | feature/cockpit-shows-evidence | the Cockpit's evidence view |
| A3 | I1957 | feature/every-test-line-is-read | per-test results and the local run index |
| F2a | I1958 | feature/evidence-has-routes | one evidence route per subject, and panels joined to their subject |
| F1 | I1959 | feature/evidence-publication-is-typed | the site publication through evidence publish, bound to its build and kept on gh-pages |
| F2b | I1960 | feature/every-widget-names-its-proof | the badge on every widget, and the delivery report's test dimensions |
| G | I1961 | feature/deploy-verifies-evidence-routes | the deploy check of the published evidence routes |
| H | I1962 | feature/evidence-check-reads-widgets | the gates over subject findings and widgets (decision 10) |
| I | I1963 | feature/evidence-is-tested-end-to-end | the cases across every surface |
| J | I1964 | feature/evidence-is-documented | the evidence reference, held to the tree |
| PF | I1965 | feature/the-preflight-reads-the-one-verdict | the entry preflight onto the freshness function and the corpus verdict |
The slice order: this record, T, D, R, A1 and S577 first; then A1 → {A2, SI} → C → P → A3. The capability changes among them land one at a time, in that order. A1 is stacked on PR #597 and S577 on PR #577. The preflight's follow-up slice (I1965) follows once PR #578 and PR #597, carrying A1, are both on master.
The order in which pull requests land is the owner's. This record keeps only the constraints the design depends on, not a queue, which would go stale:
- PR #629 lands before PR #577.
- PR #577 lands only after its CI fix (S577) is merged into it and after it has merged master, so that PR #627 and PR #629 are in its base, and only after a CI run of it shows crate executions, clean producer-measured trees and a clean report tree.
- A1 lands with PR #597: merged into it, or retargeted to master once PR #597 has landed.
- PR #578 and PR #597 with A1 merge cleanly in either order.
- No slice of this design bumps the version: a pull request that moves the public surface inherits the version already on master by merging it.
PR #629 precedes PR #577 because, before it, a case that declines to run is written ok and
recorded as a pass. Recording CI's runs automatically on such a base would have industrialised
that defect, which is ADR 0080's decision 5.
Amendment, 2026-10-17: the published UI reads the run's ledger until the recording branch exists
Decision 1 makes the tracked ledger the one authority, fed by ADR 0080's recording branch. That branch is not built (I1954). So on 2026-10-16 the UI snapshot on the published site judged every test against a tracked ledger of fourteen local runs. It published 154 test claims as "not run" that the validation of the same commit had run.
Until I1954 lands, the Pages workflow's UI snapshot job puts the evidence artifact's
ledger.json in the checkout's ledger path before the UI is read:
- Which artifact. It is the artifact of the published commit or of its nearest validated
ancestor, through
scripts/pages evidence --ledger-out(I2786). Its manifest must name a commit in the published history, or nothing is copied. - How rows are judged. Nothing else changes: the rows are judged by decision 4 at the published commit. A row of an ancestor reads stale or inputs unchanged, never as a run of the commit shown.
- Scope. It is the publication's checkout only. Nothing is committed, and the tracked ledger in the repository is untouched.
- The check.
scripts/pages published-evidencethen refuses a publication whose report still counts morenot runclaims than.ai/repo/evidence/published-not-run.txtdeclares (ruleproject.published-evidence-is-what-ci-recorded).
When the recording branch carries CI's runs into the tracked ledger, the overlay adds nothing, and it is removed with the issue that builds the branch. The check stays.