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). 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. When every validator traces to a single trust root the number still reports, with that single root named on it: one operator could have minted every key, so the count rests on distinct model or human lines inside one trust domain, not on independence across operators. The axis reads TAMPERED in one case. More than one self-signed root is enrolled, which no code path creates. The chain walk refuses every keyid in the table the moment a second one exists, so every enrolment-dependent count was assembled from checks that all answered false. The discarded number moves to the residual for forensics rather than being offered as the value, because a number computed on a broken substrate is worse than no number.

Why no word sits on a claim

Nothing stored on a claim ranks it. There is no level, no read filters on one, and no field a writer could raise. If you have REPLICATED or ESTABLISHED in your code, reading mareforma.REPLICATED or mareforma.ESTABLISHED raises AttributeError: the names resolve to nothing, so code carrying one fails where you can see it rather than reading a word that means nothing.
Two reasons, and both are the point of the project. A single word never carries what a reader needs. “Two keys agreed” is a convergence marker, not proof of independence, and a reader handed one word takes it for the other. What you want to know is how many pairwise-distinct (model, data, signer) checks stand behind a finding, and that is a count, not a label. And a word on a row is unsigned. Any process with write access could raise it, so every read would have to re-derive whether the word was backed by signed material before serving the row. If the derivation is what you trust, serve the derivation and skip the word. Nothing replaces it with another word. What a finding is worth is computed on every read, from signed material, and reported as a number with a stated residual.

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. The standing axis is where you read whether a human signed off. It reads off the signed validation envelope, not off any word stored beside it: UNRATIFIED is the resting state, and it is not a criticism of the claim. Most findings in a working project have never been through a human, and the independence number is what a reader weighs meanwhile. 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 any validation attestation, then prints the trust map. A signature says who signed; the attestation is held to who was entitled to make it, which is the same rule the write path applies. An issuer does not corroborate a claim it signed, and neither an llm validator nor a claim’s own asserter can sign off on it. 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

Every row carrying signed material is re-verified on read: its asserter bundle always, and its validation envelope where it has one. A query_* call excludes a row whose signature does not verify; get_claim returns it flagged verified=False. Read never raises. A row with no envelope carries nothing to check and is exempt, which is what keeps a graph written before signing readable. Rows 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.

Recording that a human signed off

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. It changes nothing you can filter on, and it is terminal: the row holds one envelope, so a second validation is refused rather than written over the first. Recording a second reviewer’s view means recording it as its own claim, not over the top of theirs. validate() raises ValueError if no signer is loaded, if the loaded signer is not enrolled, if the claim has been retracted or contradicted, or if somebody has already validated it. It raises SelfValidationError if the loaded key signed the claim itself: a sign-off needs a witness who did not assert the thing being signed off on.

What trust is not

A validation does not mean the claim is true. The graph records what agents assert, what the provenance structure supports, and who put their name to it. Two agreeing findings can both be wrong if both agents were reasoning from the same false prior, which is exactly what the independence axis is for. Trust in Mareforma is a structural property: a measure of how the finding was reached, not of whether it is correct.