Skip to main content
A claim is free text with a self-declared classification and self-declared supports[] / contradicts[] edges. That is enough to track provenance, but it cannot answer one question: does the result actually support or contradict the hypothesis? An agent can write a refutation next to a confirming number and the graph records it faithfully. The trust layer (mareforma.trust) closes that gap. The same assertion is expressed as a falsifiable proposition, bound to a pre-registered prediction, measured by an effect estimate. The direction of evidence (the bearing) is computed, never declared. A count over independent data then derives a status. It is additive. Every finding still rides a signed claim as its attestation (who asserted it, when), so it appears in query() with a support_level exactly like any other claim. The trust layer adds the structured meaning on top.

The proposition: the unit of sameness

A Proposition is a truth-apt, observer-independent claim about the world, built from typed parts rather than a sentence:
It is content-addressed. Two agents who assert the same thing in different words converge on one node:
  • content_id is the answer: sha256 over the normalized (subject, relation, object, scope, direction, magnitude). Same truth conditions produce the same id, on any host, in any language (tokens are NFC-normalized, casefolded, whitespace-collapsed; the byte serialization is RFC 8785).
  • frame_id is the question: the same hash with direction and magnitude dropped. Two propositions share a frame when they are about the same question. They are the same proposition when their direction and magnitude also match; they contradict when their directions are contraries (INCREASES vs DECREASES, PRESENT vs ABSENT).
A proposition must be falsifiable to anchor evidence: it must commit to a direction (not UNSPECIFIED) and state a non-empty scope. An unscoped, directionless assertion forbids no observation and is refused.

The prediction: a pre-registered rule

A Prediction is the decision rule, bound to the proposition before the numbers are seen. It is what makes the bearing earned rather than chosen.
Two gate types ship today, both closed-form (no iterative estimation, so no cross-host float drift):
  • Superiority declares the predicted side of the null (direction_of_interest).
  • Equivalence (TOST) declares an equivalence region (equivalence_lower / equivalence_upper) around the null, for testing a NO_EFFECT proposition.

The estimate: a value plus its uncertainty

An EffectEstimate carries the result and exactly what the gate needs:
Supply a p_value, a full (ci_lower, ci_upper, ci_level) triple, or both. The estimate runs its own consistency checks on construction and refuses inconsistent input (a CI that does not bracket the estimate, a non-finite bound, a p_value outside [0, 1]) rather than storing it.

The bearing: computed, not declared

compute_bearing is the gate. It reads the pre-registered rule and the realised estimate and returns supports / refutes / neutral. The agent never picks the label.
The test is one-sided at alpha (the direction is pre-registered). The estimate is significant when p_value < 2*alpha (the supplied p is two-sided by the metafor/escalc convention, so the one-sided alpha level is 2*alpha), or, with no p, when the (1 - 2*alpha) CI excludes the null. The sign of estimate - null is then compared against the pre-registered direction. For equivalence, a CI lying entirely inside the region supports the no-effect proposition, entirely outside refutes it, and straddling a margin is neutral. Because the label is a function of the registered rule and the number, an agent cannot relabel a refutation as support.

The status: derived from independent data

Status is a count over independent lines of evidence on a single content_id, not an assertion of truth and not a human gate. Independence is a distinct-signer heuristic: two supporting lines count as independent when they come from different signers (the claim’s signing key) and different datasets (data_id). Unsigned lines fall back to distinct generated_by run. One signer (or, for unsigned lines, one run) contributes at most one independent support and one independent refute, so a single signer cannot reach CONVERGENT on its own. A finding may carry several evidence lines, and each line’s bearing is recomputed on read. REFUTED and CONTESTED are derived labels, not auto-refutation: REFUTED means “no surviving independent support,” not “this proposition is false.” Status is a versioned policy (status_policy@v4) over durable stored counts, never baked into the schema. Improving the rule later is a new policy over the same data, not a migration.

Frame-level contest

Separately from a proposition’s own status, its frame is contested when a contrary proposition in the same frame has at least one independent supporting line. The contest is surfaced at read time; it does not silently corroborate or refute either side.

Recording a finding

assert_finding ties it together in one call: it validates the inputs, computes the bearing, writes a signed claim as the attestation, persists the evidence tree, and derives the status.
A second signer on a distinct dataset adds the second independent line and lifts the proposition to CONVERGENT. Reusing the same signing key would not: one signer contributes at most one independent support, so the second finding has to be signed under a distinct key. Open the graph under a second key_path:
A finding with no observed model call carries no model constraint, so a distinct signer and dataset alone converge here. When both checks do record their model lineage, a same-model rerun collapses back to one line; the read-side independence axis (graph.trust_map) is where that model distinctness is surfaced. See Grounding. assert_finding is idempotent on (content_id, data_id): re-asserting the same finding on the same dataset returns the prior finding rather than double-counting it.

Pre-registering the plan

assert_finding is the one-shot. When the pre-registration should be real (the decision rule committed and signed before the numbers exist), split it into its two earned steps:
register_plan writes the prediction with preregistered=1 and a signed plan attestation claim of its own. submit_finding then binds the outcome to that plan: the finding’s signed supports[] cites the plan attestation, so the plan → finding edge is cryptographic rather than denormalised metadata. The gap between committing the rule and seeing the result becomes a verifiable pre-registration, not a convention. assert_finding composes both in one call and flags its synthesised plan preregistered=0, so a genuine up-front pre-registration stays distinguishable from a one-shot.

How this relates to support levels

The two layers answer different questions and run side by side:
  • support_level (PRELIMINARY / REPLICATED / ESTABLISHED, see Trust) is about a claim’s provenance: did independent agents converge on a shared upstream, and did a human validate it.
  • Status (UNTESTED, PRELIMINARY, CONVERGENT, REFUTED, CONTESTED) is about a proposition’s evidence: how many independent datasets support or refute it, with the direction of each computed from a pre-registered rule.
A finding has both. The claim records who asserted what; the trust layer records whether the data earns it. A third axis asks whether the cited data actually flowed into the finding at all, computed from execution rather than declared: see Grounding. See the API reference for full signatures and the data model for the stored tables.