Skip to content

Evidence is indexed by subject and judged at the presented revision

Evidence is indexed by subject and judged at the presented revision

majordomus://adr/adr-0087proposed evidenceclaimsciverificationsite

it references

referenced by

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 show judges the working tree of whoever runs it. The site is built from one commit, which site/data/build.json names, 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 record consumes, 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 to failing, so a test that declined to run is reported as counter-evidence. In the other direction, the suite runner writes ok for 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.rs has its own changed_since and its own mapping from an execution to a state, beside the one in evidence/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.tests check 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. Its verification.enforcement check recounts rules::report instead 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 current is 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 ci against 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 ci and derives the report through majordomus evidence show." Amended: the collector derives the report first, with evidence show over 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: current takes this record's definition (decision 3). Until the site publication is typed, scripts/pages evidence publishes current only 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 publishes stale (an ancestor's run, or a run that recorded a failure) or unknown (the run cannot confirm the report), naming each reason. The rule can only withhold current; it never changes the report.
  • ¶4, "That publication finds evidence of its own commit and says current." Amended the same way: the dispatched publication says current only 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/pages call evidence publish. From then on, the site's publication carries the first matching row of decision 3's publication table:
    • ¶3, "scripts/pages evidence runs before the build, finds the evidence artifact of master's validation recorded against the commit being published or its nearest ancestor, and writes site/data/evidence.json." Amended: the script calls evidence publish in the build's checkout, which reads that run record from the artifact or from its gh-pages copy under evidence/<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 is dirty, stale, pending or unknown.
    • ¶3, "No evidence at all is published as unavailable, with the reason." Amended: no evidence at all is pending while P's CI run has not concluded and unknown after that, naming the reason. unavailable is 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.
  • Standing, by sentence:
    • in ¶1, "CI evidence is an artifact of the run that produced it."; the sentence naming the evidence job 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 current one stands too;
    • in ¶4: the refresh as a dispatch, not a chain; the dispatch of pages.yml while the run's commit is still master's tip; and pages.yml triggered 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.

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 record call and kept in the CI artifact, on gh-pages under evidence/<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 record keeps 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

SurfaceVerdict read fromPresented revisionSupplementary records
evidence show; evidence.report over the CLI, HTTP and MCPthe tracked ledger in the working tree; with --presented, the ledger as committed in the presented committhe 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 ledgerthe working treenot read. The corpus verdict is proven only when every blocking rule is proven (decision 4)
evidence subjectthe tracked ledger, through the subject index (decision 9)the working tree, or --presentedthe 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 runnone: they show supplementary records onlyas above; coverage is current only as decision 12 saysthe local run index; --from
The gates: scripts/evidence-check, evidence show --checkthe tracked ledgerthe checked-out treenever
The Cockpitthe tracked ledger, through the subject indexthe working treea separate, advisory local-run column, through the monotone rule
The site: evidence publish, run in the build's checkout of Pthe ledger committed in Pbuild.json.commit, capped by build.json.dirtyP'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 sectionthe tracked ledger as committed at the run's commit, derived by the CI evidence job before it records the run's executionsthe published commit: current needs the run's commit to equal it, and an ancestor's run is stalethe 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 8the 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 verdictthe working treenot 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 committhrough 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.jsonstamps the E and T its run measured (decision 6); until that lands, HEAD and the recorder's own treewrites 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 publicationState
1build.json.dirty is not falsedirty
2a run record read, P's own or an ancestor's, caps at least one verdict shown through the monotone rulestale
3P's own run record is read, and every tree it carries was measured cleancurrent
4P's own run record is not read, and P's CI run has not concludedpending
5anything 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 foundunknown, 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 dirty and 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 is dirty, stale when the record capped a verdict, and otherwise pending or unknown, 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 as stale only because there the transcript itself is the ancestor's; this table replaces that rule when the interim section retires (decision 1).
  • pending is 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.

#ConditionState
1no testno_test
2no runner drives the pathunrunnable
3nothing recordednot_run
4skipnot_run, with the reason
5fail, timeout or errorfailing
6pass; git cannot comparestale
7pass; E not containedstale
8pass; contained; the test's digest moved; no input changedstale
9pass; contained; a declared input changedstale
10pass; contained; changed' empty; recorded tree clean; presented tree cleanproven
11as row 10, but the recorded tree is dirty or unknowninputs_unchanged
12as row 10, but the presented tree is dirty or unknowninputs_unchanged
13pass; contained; changed' non-empty; inputs known and none changedinputs_unchanged
14pass; contained; changed' non-empty; the route names no inputsstale

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, and build.json.dirty caps 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 unknown when there is no measurement.
  • A pull-request run's commit is GitHub's merge commit, with head_sha beside 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:

  • failing if 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:

RuleStateProofStateCarries proofWhy
proven, inputs_unchanged, stale, failing, not_run, unrunnablethe same wordyes (unrunnable only when the rule itself judged it)identical meaning
gatednot_runyesa gate refuses violations, but no recorded run proves the behaviour
reviewedno_testnothe rule declares that nothing executable expresses it
unprovenno_testyesa blocking rule that names nothing is a defect and must weaken its subject
danglingunrunnableyesa 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.

KeyResolution
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 generate writes the index as site/data/registry/evidence-subjects.json, in the executable's site-data directory. It never writes into site/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 --check stays stable.
  • The site only joins to it: scripts/generate-site-data joins 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 tests of commands.json in scripts/generate-site-data, the header reading of lib/commands.sh, and scripts/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 ci check does not need the evidence job: 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.
  • proven at 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 from not_run to inputs_unchanged (row 13) or stale (rows 9 and 14), not to proven. ADR 0080's expectation that a recording moves claims to proven, a rendered "verified" on the site, and the delivery dimension required_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 binding required_tests_current and 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_run with its reason instead of failing, and a guaranteed claim whose test only skipped is still reported as unsupported.
  • The rules corpus verdict proposed in PR #597 reaches proven less often: a corpus whose blocking rules are merely inputs_unchanged reads 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 plan derives 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-check decides 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 current for a tree the run did not vouch for.

Anti-goals

Anti-goalMechanism
a badge verified for a revision it did not testfreshness rows 6, 7 and 10–12; the site-build refusal (I1959); the deploy check (I1961)
a recorder vouching for a tree it did not seedecision 6
a run record published as a verdictdecision 1's amendment of ADR 0068
a partial run recorded as a passthe crate count rule (decision 6)
a skipped case recorded as a passthe skip word lands before CI recording (Sequence)
a failure hidden behind an absencedecision 8
a corpus verdict proven over rules whose inputs are merely unchangeddecision 4
a recording that records its own landing, without enddecision 1's stop at a covered commit
a merge left unrecorded behind a ledger-only commitdecision 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 coversdecision 3's first-match publication table
a surface greener than the ledgerthe monotone rule
a named revision judged proven over a clean failing run the checkout holds uncommittedthe 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 templatethe evidence validator (I1962) and 28_no_hardcoded_values
a widget without a subjectthe generation refusal (decision 9)
a second inventory of subjectsdecision 9
a secret publisheddecision 14
evidence lost when artifacts expiregh-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:

SliceIssueBranchWhat it delivers
ZI1945feature/evidence-subject-adrthis record, the milestone and the issues
TI1946feature/verdicts-are-status-wordsthe verdict words filed as status tokens (decision 13)
DI1947feature/every-feature-reaches-a-testevery use case owned by a feature, and the Cockpit and deployments features reaching a test
A1I1948feature/evidence-is-judged-at-the-presented-revisionthe freshness function, the presented revision, containment and aggregation (decisions 4, 5, 7 and 8)
S577I1949fix/the-ci-evidence-is-recorded-cleanPR #577's report derived before recording, its producers' trees measured, and the interim current rule (decision 1)
RI1950feature/evidence-output-is-redacted-oncethe one redactor (decision 14)
A2I1951feature/evidence-runs-are-typedrun records, measured provenance, coverage records and the crate count rule (decisions 2, 6 and 12)
SII1952feature/evidence-subjects-are-derivedthe subject index (decisions 9 to 11)
CI1953feature/evidence-answers-by-subjectthe subject, coverage and run commands
BI1954feature/every-runner-reaches-the-ledgerthe recording branch, its stop at a covered commit, and every runner's measured report (decisions 1 and 16)
PI1955feature/evidence-publication-is-a-projectionthe allow-listed publication and its first-match states (decisions 3 and 14)
EI1956feature/cockpit-shows-evidencethe Cockpit's evidence view
A3I1957feature/every-test-line-is-readper-test results and the local run index
F2aI1958feature/evidence-has-routesone evidence route per subject, and panels joined to their subject
F1I1959feature/evidence-publication-is-typedthe site publication through evidence publish, bound to its build and kept on gh-pages
F2bI1960feature/every-widget-names-its-proofthe badge on every widget, and the delivery report's test dimensions
GI1961feature/deploy-verifies-evidence-routesthe deploy check of the published evidence routes
HI1962feature/evidence-check-reads-widgetsthe gates over subject findings and widgets (decision 10)
II1963feature/evidence-is-tested-end-to-endthe cases across every surface
JI1964feature/evidence-is-documentedthe evidence reference, held to the tree
PFI1965feature/the-preflight-reads-the-one-verdictthe 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-evidence then refuses a publication whose report still counts more not run claims than .ai/repo/evidence/published-not-run.txt declares (rule project.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.

Where this comes from

canonical file
.ai/repo/adrs/0087-evidence-is-indexed-by-subject-and-judged-at-the-presented-revision.md
identity
adr-0087
also served as
MCP resource majordomus://adr/adr-0087 · majordomus entity show adr/adr-0087 · GET /api/v1/entity?uri=majordomus://adr/adr-0087
in the Cockpit
/cockpit/objects/adr/adr-0087

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.