Skip to main content
Trust in a claim is a property of its position in the graph, not a property of the agent that made it. There is no confidence score. An agent that is wrong is still confident. The graph does not ask the agent how sure it is; it tracks what the agent can demonstrate. Trust reads off two axes Mareforma derives on every read, not off a stored label: Status per content_id is the state of the answer (UNTESTED -> PRELIMINARY -> CONVERGENT, plus REFUTED and CONTESTED), and question_status per frame_id is the state of the question (consistent / divided). Both come from graph.proposition_status(prop). The support_level ladder below is the legacy stored promotion axis; its REPLICATED and ESTABLISHED public labels are deprecated for v0.4.0. See Findings for the answer/question structure the axes sit on.

Independence is the signal

The property Mareforma leads with is effective independence: the number of pairwise-distinct (model, data, signer) checks behind a finding. Two checks on the same model are one line of evidence, not two, so the count holds until a genuinely different model, or a human check, raises it. mareforma map <claim> reports the number and marks it UNVERIFIABLE when the model lineage is too soft to tell distinct checks apart, or when every validator traces to a single trust root. Read the number, not the label: REPLICATED and ESTABLISHED resolve this release as deprecated one-release aliases.

Support levels

REPLICATED / ESTABLISHED are deprecated public labels this release, kept working as a one-release alias; PRELIMINARY stays a stored support_level. All three still fire and gate as described below, but the effective-independence number above is the signal to read.
One agent claimed it. Could be correct. No independent confirmation yet.Set automatically at INSERT. Every new claim starts here.
≥2 claims signed by distinct keys cite the same upstream in supports[], AND at least one of those upstreams is itself ESTABLISHED. Set automatically at INSERT. No extra step required.The independence unit is the signing key (the asserter_keyid denormalized from each signature bundle), not generated_by. An unsigned claim (no asserter_keyid) or two claims signed by the same key cannot reach REPLICATED.REPLICATED is a convergence marker, not proof of independence. Distinct keys prove the asserters are cryptographically separate, not that their data or reasoning is independent, and signing keys are operator-mintable, so one operator can produce any number of “distinct” signers. Treat REPLICATED as a weak prior; read independence from its own axis (see The independence axis below), which is honest about what it cannot verify.The ESTABLISHED-upstream rule enforces that replication-of-noise is not replication. A fresh project bootstraps the chain via a seed claim (see below).Promotion is also gated on the claim’s transparency_logged flag. That flag holds a claim back only when the graph was opened with a rekor_url and submission has not yet succeeded; graph.refresh_unsigned() retries it. With no rekor_url the flag is set at insert, so a signed claim converges on its signature alone and the flag imposes no gate.graph.require_rekor_witnessing() is a root-signed declaration, not a live gate. It is enforced on recovery by restore(..., enforce_rekor_policy=True), which refuses to mark a signed claim convergence-eligible without a verified, claim-bound inclusion proof.
A human (an enrolled validator) reviewed the provenance chain and validated it. Only reachable via graph.validate(). No agent can self-promote.This is the gate for consequential actions.

How REPLICATED fires

Three conditions must all be true:
  1. Both claims reference the same upstream in supports[]
  2. Each claim is signed by a distinct key (asserter_keyid)
  3. At least one shared upstream is itself ESTABLISHED
One key asserting the same claim a thousand times is still PRELIMINARY. Cryptographic separation of asserters is what matters, not count. An unsigned claim carries no asserter_keyid and cannot reach REPLICATED.

Bootstrapping a fresh project (seed claim)

On a brand new graph, no claim is yet ESTABLISHED, so the third condition above can never be satisfied. An enrolled validator (typically the project’s root, auto-enrolled on first open) bootstraps the chain by asserting a seed claim: a claim inserted directly at ESTABLISHED with a signed seed envelope.

Data equality: a collapse check, not a match requirement

artifact_hash (a SHA-256 hex digest of the output bytes: figure, CSV, model) is a secondary collapse check, never a match requirement. Two peers with equal data (equal artifact_hash, or equal content-addressed data_id) collapse to one line: a byte-identical rerun is not corroboration. Distinct data counts as independent. Absent data (NULL) never blocks promotion; distinct signers alone promote. The hash is part of the signed payload, so an attacker who edits the column without the private key breaks verification. For operators who want data-distinctness as a hard gate rather than a collapse check, open with strict_promotion=True: a pair then promotes only when BOTH sides carry non-NULL data. It is off by default and only ever adds the requirement. It never loosens the default signer-axis rule. The gate is project state, not a session option. Asking for it has the root validator sign a one-way policy record, so every later opener promotes under it, restore rebuilds it, and a second session cannot promote around it by opening without the flag. Only the root key can declare it: a keyless or non-root caller gets a ProjectPolicyError rather than a gate that binds nothing but its own writes.

The independence axis

Independence is reported as its own axis, separate from the support labels, because a distinct-key rule proves cryptographic separation, not independent lines of evidence. Mareforma counts the effective independence of a finding: the number of pairwise-distinct (model, data, signer) checks behind it, read from the model lineage that observe() captures at the call boundary. A remote call is identified by its recognized provider host; a local model by the served weights’ digest, so two local models are told apart by their weights, not a self-chosen name. Two checks on the same model do not both count; a genuinely different model, or a human check, raises the number. Where the model lineage is too soft to tell distinct checks apart, or every enrolled validator traces to a single trust root (the solo-operator default, where the same operator could have minted every key), the axis is UNVERIFIABLE. Mareforma states that plainly rather than letting “two distinct signatures” read as “two independent labs.” mareforma map <claim> renders the axis, and it never translates a convergence marker into the word “independent.”

What enrolling a key costs

A project can start with no key. Its claims carry no signer, and the trust gate counts them on a retired run axis keyed on generated_by, so the graph still reads. The moment you enrol a validator, that stops. A project that signs does not count unsigned claims toward independence, including the ones it wrote before it had a key. Those findings do not disappear from the graph, but they stop counting: the proposition reads UNTESTED with lines_skipped equal to the number of lines dropped, and each drop is named unregistered_signer_skipped in .mareforma/health.jsonl. There is no un-enrol. Plan for this before you open a project with a key. The reason is that generated_by is a value the writer chooses. Once a project signs, a writer with database access can add an unsigned claim and pick any run token, and the gate cannot tell that claim from the honest ones written before the key existed. Two attacks follow directly: stripping the signature off an existing claim, and inserting a fabricated unsigned tree into a signed project. Both read as convergent support before the rule, with nothing disclosed. So the rule trades a real cost for a real defence, and the cost is disclosed rather than silent. If you intend to sign a project, enrol the key first and write afterwards.

The trust map

A claim carries many trust properties, and they are not equal. Some Mareforma computes from evidence (COMPUTED); some through a proxy whose bound it names (PROXIED); some it does not evaluate at all and says so (DEFERRED). mareforma map <claim> places each property (attributability, provenance, grounding, faithfulness, methodological validity, leakage, independence, contestation, standing, trust-root, witnessing) at its tier with the residual named, so trust reads as a structured ledger instead of one word.
A property Mareforma cannot observe is stated (DEFERRED, not present, or UNVERIFIABLE), never inferred to a confident answer. The map is a read-side artifact: it adds no signed field and changes no verdict. Every map carries the engine version that computed it, so a stored record names the build behind its tiers. That version is bound to the package version, and a build where the two disagree refuses to render a map at all, raising TrustMapVersionError. A map whose tiers came from one engine while naming another would be worse than no map, because the name is what a later reader checks the tiers against.

Faithfulness: re-execution as a proxy

Faithfulness answers a narrow question: re-run the recorded pipeline in a clean scope, does the reported number come back? mareforma reexec <run> takes a recorded run, re-executes it, and returns a three-valued verdict: REPRODUCED, DIVERGED, or COULD_NOT_REEXECUTE. The third value is the honest one. A run that rests on world contact, private data, or expensive compute, or whose entry point will not resolve, raises, or returns a non-number, is COULD_NOT_REEXECUTE, never a false REPRODUCED and never a spurious DIVERGED. A re-run that could not run is not evidence that the number changed. Faithfulness is a PROXIED axis, and the residual names its bound: reproducible is not correct, and re-running the same pipeline on the same inputs is not an independent line of evidence. mareforma reexec <run> --map <claim> places the verdict on that claim’s trust map as a read-side overlay, so it reads next to every other property without being confused for truth or independence.

Verify

mareforma verify <target> is the audit receipt at the moment of the check. It re-verifies signatures, the grounding-to-citation binding, and the support level, then prints the trust map. A directory target is an export dir and a target that is not a path is a claim id resolved against the local project. A file is told apart by its DSSE payloadType, never by its name: a per-finding audit receipt and a signed audit-run record each reach their own verifier, and everything else falls through to the signed-bundle verifier. A receipt is signed by the auditor, so pin their key with --key; without it the check reports exit 2, unverifiable, rather than a verdict. Verifying a claim needs only public material; an auditor does not need the producer’s private key. Exit codes are stable, for CI gates: A missing local key makes a bundle unverifiable (2), never a failure (1): the auditor lacks the key, the bundle is not proven tampered. Code 3 is not a verdict at all, so a gate fails the build on it rather than reading it as one. --json emits the verdict and trust map for a gate to parse.

Verify on read

High-trust rows (REPLICATED and ESTABLISHED) are re-verified on read. A query_* call excludes a row whose signature does not verify; get_claim returns it flagged verified=False. Read never raises. ESTABLISHED rows in query results and bundle export also disclose single_trust_domain: whether every validator traces to one root of trust. This is a disclosure of trust-domain concentration, not a Sybil guard.

How ESTABLISHED is reached

graph.validate() is identity-gated. The graph must have a loaded signing key AND that key must be enrolled in the project’s validators table. The first key opened against a fresh graph auto-enrolls as the root validator (silent self-signed enrollment with a UserWarning). Additional validators are added via mareforma validator add or graph.enroll_validator(). The validation event itself is signed: a DSSE-style envelope binding (claim_id, validator_keyid, validated_at) is persisted to the claim’s validation_signature column. validated_by is a cosmetic display label; the authoritative identity is the keyid embedded in the signed envelope. validate() raises ValueError if the claim is not yet REPLICATED, if no signer is loaded, or if the loaded signer is not enrolled. It raises SelfValidationError if the loaded key signed the claim itself, or signed any peer in the set that converged it: promotion needs a witness who asserted none of the lines being promoted.

What trust is not

ESTABLISHED does not mean the claim is true. The graph records what agents assert and what the provenance structure supports. A REPLICATED finding can still be wrong if both agents were reasoning from the same false prior. Trust in Mareforma is a structural property: a measure of how the finding was reached, not of whether it is correct.