Skip to content

HTTP API: evidence

What actually ran, against which commit, and whether it still proves anything. The claims matrix binds a claim to a test by path; the ledger under .ai/repo/evidence records the latest execution of every test with the commit, the tree state, the digest of the test's own source, the time, the origin and the command that runs it again. Joining the two answers, per claim, whether the repository can honestly call it proven — and distinguishes a run recorded against this very commit from one whose inputs merely have not changed since, because collapsing those two is how a green badge stops meaning anything.

Rendered from docs/generated/openapi.json, which the executable generates from its capability registry and CI refuses stale. The raw document is at /openapi.json; a running server serves the same document at its own /openapi.json and Swagger UI at /swagger.

evidence

What actually ran, against which commit, and whether it still proves anything. The claims matrix binds a claim to a test by path; the ledger under .ai/repo/evidence records the latest execution of every test with the commit, the tree state, the digest of the test's own source, the time, the origin and the command that runs it again. Joining the two answers, per claim, whether the repository can honestly call it proven — and distinguishes a run recorded against this very commit from one whose inputs merely have not changed since, because collapsing those two is how a green badge stops meaning anything.

GET /api/v1/evidence evidence.report

Every claim against the evidence recorded for it

The whole claims matrix joined to the ledger and judged at the presented revision: the working tree by default, or the checked-out commit as committed, read from the ledger that commit holds. Per claim: the proof state, the sentence explaining how that state was derived, why when the state alone does not say, the execution behind it, the files that changed since it, and the command that produces it again. A run on a commit the presented revision does not contain is stale, a test that declined to run is not run, and a run the working ledger holds that the presented commit's does not can only withhold proven. The tallies count the whole matrix even when the answer is filtered, and the findings name every claim that declares a guarantee the evidence does not support. Kept per checkout state, never longer: the ledger is a file that changes outside this process, and an edit of it, of a test or of any tracked file, a new untracked file, a commit or a new ref is a new answer.

  • query
  • behaviorally_verified
  • benchmark required
  • cache checkout (max_entries=8)
  • MCP tool majordomus_evidence
  • MCP resource {"name": "evidence", "uri": "majordomus://evidence"}
  • $ evidence show

Query parameters

nametyperequireddescriptionexamples
state ProofState | null no Only claims in this proof state (`proven`, `inputs_unchanged`, `stale`, `failing`, `not_run`, `unrunnable`, `no_test`). Absent: every claim.
status string no Only claims declaring this status (`guaranteed`, `advisory`, `planned`, `rejected`). Absent: every claim.
findings_only boolean no Only the claims whose declared status the evidence does not support, with the findings. The tallies still count the whole matrix, so a filtered answer never misreports how much of it was examined. findings: true
presented string no Judge at this revision, as committed, rather than at the working tree: `HEAD`, or any name of the checked-out commit. It must be the checked-out commit; the ledger is read as that commit holds it, and a run the working ledger holds that the commit's does not can only withhold `proven`. Absent: the working tree.
presented_tree EvidenceTreeState | null no What the caller knows about the presented commit's tree (`clean`, `dirty`, `unknown`). It can only weaken the measured state, never assert a clean tree over a dirty checkout, and it means nothing without `presented`.

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/evidence?findings_only=true"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 EvidenceReport Every claim against the evidence recorded for it: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/evidence/claim evidence.claim

What proves this claim

One claim with its proof state, the execution behind it — outcome, duration, commit, tree state, digest, time and origin — the command that reproduces it, and the other claims the same test proves. A claim the matrix does not declare is a not-found rather than an empty answer, because a typo that read as 'this claim has no evidence' is the one answer this capability must never give.

  • query
  • behaviorally_verified
  • benchmark required
  • cache checkout (max_entries=32)
  • MCP tool majordomus_evidence_claim
  • $ evidence claim

Query parameters

nametyperequireddescriptionexamples
claim string yes The claim id, as `docs/CLAIMS.yaml` spells it. first-claim: "a-candidate-is-routed-by-what-it-is-about"

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/evidence/claim?claim=a-candidate-is-routed-by-what-it-is-about"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 ClaimEvidence What proves this claim: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.
GET /api/v1/evidence/test evidence.test

What this test proves

One test — by identity or by the path a claim names it with — with its latest execution, whether its source still hashes to what that execution recorded, the command that runs it again, and every claim that names it. This is the reverse of evidence.claim and reads the same derivation, so the two directions cannot disagree.

  • query
  • behaviorally_verified
  • benchmark required
  • cache disabled
  • MCP tool majordomus_evidence_test
  • $ evidence proves

Query parameters

nametyperequireddescriptionexamples
test string yes `suite:84_distribution_model`, `crate:why`, or the path itself (`test/cases/84_distribution_model.sh`), which is resolved to the same identity. first-test: "suite:982_a_candidate_is_routed_by_what_it_is_about"

Try it

The port is whatever the server logged; 8741 is the one it asks for first. The values are the operation's first example.

curl -s "http://127.0.0.1:8741/api/v1/evidence/test?test=suite%3A982_a_candidate_is_routed_by_what_it_is_about"

Tested by apps/majordomus-cli/tests/http_serve.rs, which replays the route with these values against a server on a real socket.

Responses

statusbodymeaning
200 TestEvidence What this test proves: the result.
400 ErrorBody invalid_input: The input does not fit the schema: a parameter of the wrong type, an unknown one, or a value the capability rejects.
404 ErrorBody not_found: The input names something the repository does not hold.
500 ErrorBody internal: The capability failed for a reason of its own; the message names it.
default ErrorBody Any other failure the transport reports: 405 method_not_allowed for another method on this path, 400 invalid_input for a body that is not a JSON object.

Schemas

The components only this tag's operations use. Each row is the type's own field, its doc comment the description; a type another page owns links there.

ClaimEvidence

object
One claim with everything the repository can say about its proof, and the way back out to the test that carries it.

The proof is flattened in, so a client reads one claim rather than an evidence wrapper around one; the executions and the other claims the same test proves sit beside it.

use majordomus_cli::capability::builtin::evidence::ClaimEvidence;
use majordomus_cli::evidence::ClaimProof;

let proof: ClaimProof = serde_json::from_value(serde_json::json!({
    "id": "evidence-both-directions",
    "claim": "a claim and its test answer for each other",
    "status": "guaranteed",
    "source": null,
    "implementation": null,
    "test_path": "test/cases/84_distribution_model.sh",
    "test": "suite:84_distribution_model",
    "state": "not_run",
    "meaning": "no run of this test has been recorded",
    "execution": null,
    "changed": [],
    "reproduce": "bash test/run.sh 84_distribution_model"
}))
.unwrap();

let answer = ClaimEvidence {
    proof,
    executions: Vec::new(),
    also_proves: vec!["evidence-record-is-not-networked".to_string()],
};
let json = serde_json::to_value(&answer).unwrap();
assert_eq!(json["id"], "evidence-both-directions");
assert!(json.get("proof").is_none(), "the claim is flattened in, not nested");
assert_eq!(json["also_proves"], serde_json::json!(["evidence-record-is-not-networked"]));
// a list even where the ledger keeps one: retention is a decision that may change,
// and a client that read a bare object would break when it did
assert_eq!(json["executions"], serde_json::json!([]));
propertytyperequireddescription
id string yes The claim's id, as docs/CLAIMS.yaml spells it.
claim string yes The claim sentence.
status string yes The status the claim declares: guaranteed, advisory, planned, rejected.
source string | null no The document that defines the behaviour.
implementation string | null no The file that implements it.
test_path string | null no The path the claim names as its test, verbatim.
test string | null no The test's stable identity, when a runner owns it.
state ProofState → ProofState yes What the repository can say about the proof.
meaning string yes That state, in one sentence.
execution EvidenceExecution | null → EvidenceExecution no The execution the state was derived from, when there is one.
changed array of string yes The files this claim names that differ between the recorded commit and the working tree. Empty unless the state is ProofState::Stale.

Always emitted, empty included: the schema this capability publishes says the key is there, and a client that has to tell "no files changed" from "the server did not say" is a client reading two different answers as one.

detail string | null no Why the state is what it is, in one sentence, when the state alone does not say: a test that declined to run, a run on a commit the presented revision does not contain, a tree that was not its commit, a failing run the checkout holds uncommitted.
reproduce string | null no The command that produces the proof again, when a runner owns the test.
executions array of EvidenceExecution → EvidenceExecution yes Every execution the ledger holds for this claim's test — today one, the latest, and a list because the ledger's retention is a decision that may change and a client that read a bare object would break when it did.
also_proves array of string yes The other claims the same test proves. A test shared by twelve claims is a test whose failure is twelve findings, and a reader of one of them should be able to see the other eleven without searching.

Used by evidence.claim

EvidenceClaimProof

object
One claim, joined to whatever the repository actually recorded about it.

The claim's own fields are carried verbatim beside the derived ones, so that a reader who disbelieves the state can check the join rather than take it.

use majordomus_cli::evidence::{ClaimProof, ProofState};

let proof = ClaimProof {
    id: "scope-integrity".into(),
    claim: "a commit outside the task's scope is refused".into(),
    status: "guaranteed".into(),
    source: Some("docs/SCOPE.md".into()),
    implementation: Some("apps/majordomus-cli/src/scope.rs".into()),
    test_path: Some("test/cases/07_scope.sh".into()),
    test: Some("suite:07_scope".into()),
    state: ProofState::NotRun,
    meaning: ProofState::NotRun.meaning().to_string(),
    execution: None,
    changed: vec![],
    detail: None,
    reproduce: Some("bash test/run.sh 07_scope".into()),
};

let json = serde_json::to_value(&proof).unwrap();
// `changed` is emitted empty rather than omitted: a client must not have to tell
// "no file changed" from "the server did not say"
assert_eq!(json["changed"], serde_json::json!([]));
// an execution that does not exist is absent, not a null standing in for one
assert!(json.get("execution").is_none());
// and so is a detail the state did not need
assert!(json.get("detail").is_none());
assert_eq!(json["meaning"], serde_json::json!(ProofState::NotRun.meaning()));

Example

A ClaimProof is produced by report; its state is the whole answer and its meaning is that answer in a sentence, worded once for every surface.

use majordomus_cli::evidence::{report, ClaimProof, Ledger};
use majordomus_cli::synthetic::SyntheticRepository;
let repo = SyntheticRepository::small().unwrap();
let ledger = Ledger::load(repo.root()).unwrap();
let r = report(&repo.index().unwrap(), &ledger);
let proofs: &[ClaimProof] = &r.claims;
assert!(proofs.is_empty(), "a synthetic tree declares no claims");
propertytyperequireddescription
id string yes The claim's id, as docs/CLAIMS.yaml spells it.
claim string yes The claim sentence.
status string yes The status the claim declares: guaranteed, advisory, planned, rejected.
source string | null no The document that defines the behaviour.
implementation string | null no The file that implements it.
test_path string | null no The path the claim names as its test, verbatim.
test string | null no The test's stable identity, when a runner owns it.
state ProofState → ProofState yes What the repository can say about the proof.
meaning string yes That state, in one sentence.
execution EvidenceExecution | null → EvidenceExecution no The execution the state was derived from, when there is one.
changed array of string yes The files this claim names that differ between the recorded commit and the working tree. Empty unless the state is ProofState::Stale.

Always emitted, empty included: the schema this capability publishes says the key is there, and a client that has to tell "no files changed" from "the server did not say" is a client reading two different answers as one.

detail string | null no Why the state is what it is, in one sentence, when the state alone does not say: a test that declined to run, a run on a commit the presented revision does not contain, a tree that was not its commit, a failing run the checkout holds uncommitted.
reproduce string | null no The command that produces the proof again, when a runner owns the test.

Used by EvidenceReport TestEvidence

EvidenceFinding

object
A claim whose declared status the evidence does not support.

Only a guaranteed claim can produce one: the other statuses already say that a current proof is not what they are claiming. A finding says what was claimed, what the evidence supports instead, and — where running something would settle it — what to run.

use majordomus_cli::evidence::{Finding, ProofState};

let finding = Finding {
    claim: "scope-integrity".into(),
    status: "guaranteed".into(),
    state: ProofState::NotRun,
    reason: "the claim guarantees a behaviour and names a test, and no run of that \
             test has ever been recorded"
        .into(),
    reproduce: Some("bash test/run.sh 07_scope".into()),
};

// the gap is the finding: what the matrix declares against what the ledger holds
assert_ne!(finding.status, finding.state.label());
assert!(finding.reproduce.unwrap().starts_with("bash test/run.sh"));

Example

use majordomus_cli::evidence::{Finding, ProofState};
let f = Finding {
    claim: "some-claim".into(),
    status: "guaranteed".into(),
    state: ProofState::NotRun,
    reason: "the claim names a test nobody has run".into(),
    reproduce: Some("bash test/run.sh 07_scope".into()),
};
assert_eq!(f.state, ProofState::NotRun);
propertytyperequireddescription
claim string yes The claim.
status string yes What the claim says it is.
state ProofState → ProofState yes What the evidence supports.
reason string yes Why this is a finding.
reproduce string | null no What to run, when running something would settle it.

Used by EvidenceReport

EvidenceLedgerSummary

object
What the ledger is and whether it is there at all.

A report that did not say where its evidence came from would be asking to be believed. This is the provenance of the whole report: the file, how much it holds, how old the newest entry is, and which commits anything was ever recorded against.

use majordomus_cli::evidence::{Ledger, LedgerSummary, LEDGER_PATH};

let summary: LedgerSummary = Ledger::empty().summary();
assert_eq!(summary.path, LEDGER_PATH);
assert!(!summary.present, "a ledger holding nothing has recorded nothing");
assert_eq!(summary.executions, 0);
assert!(summary.newest.is_none());
assert!(summary.commits.is_empty());

Example

use majordomus_cli::evidence::LedgerSummary;
let s = LedgerSummary {
    path: ".ai/repo/evidence/ledger.json".into(),
    present: false,
    executions: 0,
    newest: None,
    commits: vec![],
};
assert!(!s.present, "a repository that recorded nothing says so");
propertytyperequireddescription
path string yes Where it lives, repository-relative.
present boolean yes Whether the ledger holds at least one execution.

Not whether the file exists. A ledger file holding nothing and no ledger file at all are the same answer to the only question a reader is asking — has anything been recorded — and both make every claim read not_run. Ledger::present answers the filesystem question, and nothing derives a proof state from it.

executions integer (uint) yes How many executions it holds.
newest string | null no The newest at in it, when it holds any.
commits array of string yes The distinct commits its executions were recorded against. Always emitted, empty included, for the reason ClaimProof::changed is.

Used by EvidenceReport

EvidencePresentedRevision

object
What a report was judged at: the revision, its tree, and the runs the checkout holds that the revision's own ledger does not.

A verdict is always a verdict at something. evidence show judges the working tree; evidence show --presented HEAD judges the checked-out commit as committed, from the ledger committed in it — which is the reading a site built from that commit needs.

use majordomus_cli::evidence::{PresentedRevision, TreeState};

let here = PresentedRevision {
    revision: "working_tree".into(),
    tree: TreeState::Dirty,
    uncommitted: vec![],
};
let json = serde_json::to_value(&here).unwrap();
assert_eq!(json["tree"], "dirty");
// nothing held uncommitted is absent, not an empty list to read past
assert!(json.get("uncommitted").is_none());
propertytyperequireddescription
revision string yes working_tree, or the full id of the commit judged as committed.
tree EvidenceTreeState → EvidenceTreeState yes Whether that revision's tree was its commit: for the working tree, the checkout's state; for a commit, the checkout measured ignoring the ledger's working copy and weakened by anything the caller knew.
uncommitted array of string no The tests whose execution in the working copy of the ledger the presented commit's ledger does not hold, sorted. Read only to withhold proven, never to decide a verdict; always empty for the working tree, whose ledger is itself the authority.

Used by EvidenceReport

EvidenceReport

object
The whole joined picture: every claim of the matrix against every execution recorded.

What the capability answers, what the gate judges and what the site renders are this one value; the totals and the findings are derived from the claims, never counted twice.

use majordomus_cli::evidence::{
    EvidenceReport, Finding, Ledger, PresentedRevision, ProofState, Subject, TreeState,
};

let mut report = EvidenceReport {
    head: None,
    working_tree: "unknown".into(),
    presented: PresentedRevision {
        revision: "working_tree".into(),
        tree: TreeState::Unknown,
        uncommitted: vec![],
    },
    ledger: Ledger::empty().summary(),
    subject: Subject { examined: 0, complete: true, excluded: vec![] },
    claims: vec![],
    totals: Default::default(),
    findings: vec![],
};
assert!(report.satisfied(), "nothing claimed, so nothing unsupported");

// and the same report over a subject it could not wholly read is not a pass
let mut partial = report.clone();
partial.subject.complete = false;
assert!(!partial.satisfied(), "an absent answer is not a clean one");

report.findings.push(Finding {
    claim: "scope-integrity".into(),
    status: "guaranteed".into(),
    state: ProofState::Failing,
    reason: "the claim guarantees a behaviour whose test most recently failed".into(),
    reproduce: None,
});
// one unsupported guarantee is enough: the gate is not a proportion
assert!(!report.satisfied());

Example

The whole matrix joined to the ledger. satisfied is true when no claim declares a guarantee the recorded evidence does not support.

use majordomus_cli::evidence::{report, EvidenceReport, Ledger};
use majordomus_cli::synthetic::SyntheticRepository;
let repo = SyntheticRepository::small().unwrap();
let ledger = Ledger::load(repo.root()).unwrap();
let r: EvidenceReport = report(&repo.index().unwrap(), &ledger);
assert!(r.satisfied(), "no claim, so no unsupported guarantee");
propertytyperequireddescription
head string | null no The commit the report was derived against.
working_tree string yes clean, dirty or unknown.
presented EvidencePresentedRevision → EvidencePresentedRevision yes What the verdicts are about: the working tree, or the checked-out commit judged as committed.
ledger EvidenceLedgerSummary → EvidenceLedgerSummary yes The ledger this was joined against.
subject EvidenceSubject → EvidenceSubject yes What the report could reach. Read this before reading the tallies: they are counts over the claims the index held, and an index that dropped a file holds fewer.
claims array of EvidenceClaimProof → EvidenceClaimProof yes Every claim, in the matrix's own order.
totals map of integer (uint) yes How many claims are in each state.
findings array of EvidenceFinding → EvidenceFinding yes Claims whose declared status the evidence does not support.

Used by evidence.report

EvidenceSubject

object
Whether the report could see its whole subject.

A verdict over a matrix the index silently shrank is the failure this repository met four times in one day: an object is excluded, every projection of the smaller index is a faithful projection, and each check prints a true statement about the half it can see. A freshness check answers "was this generated from this tree" and cannot answer "did the generator see everything in it", so the answer has to carry its own denominator.

use majordomus_cli::evidence::Subject;
let whole = Subject { examined: 149, complete: true, excluded: vec![] };
assert!(whole.complete);

let partial = Subject {
    examined: 148,
    complete: false,
    excluded: vec!["docs/CLAIMS.yaml: unknown_key".into()],
};
assert!(!partial.complete, "a shrunken index must not report a clean verdict");
propertytyperequireddescription
examined integer (uint) yes How many claims the index held, and this report therefore examined.
complete boolean yes Whether the index that produced them carried no error diagnostic. When this is false, every count and every absence below is about a smaller world than the repository, and no verdict over it means what it says.
excluded array of string yes The diagnostics that excluded something, when any did: <path>: <code>.

Used by EvidenceReport

EvidenceTreeState

"clean" | "dirty" | "unknown"
Whether a tree was the commit it sits on: the three words the ledger and git already use, typed.

unknown is not a softer clean. It is what is recorded when git could not be asked, and not knowing is not proof, so every rule here treats it as not clean.

use majordomus_cli::evidence::freshness::TreeState;

assert_eq!(TreeState::parse("clean"), TreeState::Clean);
assert_eq!(TreeState::parse("dirty"), TreeState::Dirty);
// a word nobody wrote down is not a clean tree
assert_eq!(TreeState::parse("spotless"), TreeState::Unknown);
assert_eq!(TreeState::Dirty.as_str(), "dirty");
assert_eq!(serde_json::to_value(TreeState::Unknown).unwrap(), "unknown");
  • cleanThe tree was the commit: nothing but the evidence ledger differed from it.
  • dirtySomething other than the evidence ledger differed from the commit.
  • unknownGit could not be asked, or the word recorded is none of the three.

Used by evidence.report EvidencePresentedRevision

TestEvidence

object
One test, and what it proves.

The other direction of ClaimEvidence, read from the test's end: its identity, its source and whether that source is still the one that ran, the command that runs it again, and every claim that names it.

use majordomus_cli::capability::builtin::evidence::TestEvidence;
use majordomus_cli::evidence::TestId;

let id = TestId::of("apps/majordomus-cli/tests/why.rs").unwrap();
let answer = TestEvidence {
    test: id.as_string(),
    runner: id.runner,
    source: id.source(),
    present: true,
    reproduce: id.reproduce(),
    execution: None,
    digest_matches: None,
    proves: Vec::new(),
};
assert_eq!(answer.test, "crate:why");
assert_eq!(answer.reproduce, "cargo test --test why");

// nothing recorded: the two provenance keys are absent rather than null, so "no
// execution" cannot be read as an execution that said nothing
let json = serde_json::to_value(&answer).unwrap();
assert!(json.get("execution").is_none() && json.get("digest_matches").is_none());
assert_eq!(json["proves"], serde_json::json!([]));
propertytyperequireddescription
test string yes The stable identity: suite:<case> or crate:<binary>.
runner EvidenceRunner → EvidenceRunner yes Which runner owns it.
source string yes Its own source, repository-relative.
present boolean yes Whether that source is in this checkout.
reproduce string yes The command that runs this one test.
execution EvidenceExecution | null → EvidenceExecution no The latest recorded execution, when there is one.
digest_matches boolean | null no Whether the test's source still hashes to what the execution recorded. None when there is no execution, or the source is not there to hash.
proves array of EvidenceClaimProof → EvidenceClaimProof yes Every claim that names this test, with the state each is in.

Used by evidence.test

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