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
AProposition is a truth-apt, observer-independent claim about the world,
built from typed parts rather than a sentence:
content_idis the answer:sha256over 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_idis the question: the same hash withdirectionandmagnitudedropped. 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 (INCREASESvsDECREASES,PRESENTvsABSENT).
UNSPECIFIED) and state a non-empty scope. An unscoped,
directionless assertion forbids no observation and is refused.
The prediction: a pre-registered rule
APrediction is the decision rule, bound to the proposition before the
numbers are seen. It is what makes the bearing earned rather than chosen.
- 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 aNO_EFFECTproposition.
The estimate: a value plus its uncertainty
AnEffectEstimate carries the result and exactly what the gate needs:
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.
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.
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. That key has to be a registered
validator: a line counts on the distinct-signer axis only when its signer is
enrolled and its signature verifies, so an unregistered key cannot manufacture a
second independent line. Enroll the second key under the root, then sign with it:
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.
The two derived axes, and the legacy ladder
Trust is read off two axes Mareforma derives on every read, one per level of the answer/question structure above. Read these, not a stored label:Status(UNTESTED,PRELIMINARY,CONVERGENT,REFUTED,CONTESTED) is the state of the answer, computed percontent_id: how many independent datasets support or refute this exact proposition, with the direction of each computed from a pre-registered rule.question_status(consistent/divided) is the state of the question, computed perframe_id: whether the propositions sharing a frame agree or point in contrary directions. It comes from the same computation as the underlyingFrameStatus, so the two cannot disagree about whether the frame is divided.
graph.proposition_status(prop) returns both, alongside a frame_status key
that echoes the answer’s own word and is deprecated for v0.4.0 in favour of
question_status.
The legacy layer is support_level (PRELIMINARY / REPLICATED /
ESTABLISHED, see Trust): the stored promotion ladder on a
claim, recording whether independent agents converged on a shared upstream and
whether a human validated it. Its REPLICATED and ESTABLISHED public labels
are deprecated for v0.4.0; the derived axes above are what a reader should reach
for.
A finding carries all of it. The claim records who asserted what; the derived
axes record 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.