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.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 thatobserve() 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 ongenerated_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.
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. Aquery_* 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.