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.PRELIMINARY
PRELIMINARY
One agent claimed it. Could be correct. No independent confirmation yet.Set automatically at INSERT. Every new claim starts here.
REPLICATED
REPLICATED
≥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.ESTABLISHED
ESTABLISHED
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
- Both claims reference the same upstream in
supports[] - Each claim is signed by a distinct key (
asserter_keyid) - At least one shared upstream is itself ESTABLISHED
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 atESTABLISHED 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 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.
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. Aquery_* 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.