/data/trial.csv, fall back to what it remembers from training, and write a
number that never touched the file. The claim records the citation faithfully and
the reader has no way to tell the difference. This is the silent fallback, and it
is the failure the grounding observer exists to catch.
mareforma.observe computes a second, separate signal: did the cited data
actually flow into the code that authored this finding? The answer is derived
from execution, never from what the producer declares. It is one of three states.
OPAQUE is first-class on purpose. A confident GROUNDED or UNGROUNDED across a
boundary the observer cannot see would be confidently wrong, which is worse than
admitting the blind spot.
Observing a scope
Wrap the span that authors a finding inobserve(...). Inside it, wrapped loaders
record what data flowed: builtins.open and io.open (which reaches
pathlib.Path.open / read_text / read_bytes), sqlite3, and, once you import
them, pandas, polars, httpx, requests, and the scientific readers
h5py / pyarrow / netCDF4. A PEP-578 audit hook records the reads and
boundaries the loaders cannot see. On exit the observer computes the verdict from
what it captured.
data_source= names the location the finding cites, which is what the verdict is
bound against. Omit it and pass only a free-text data_id, and there is nothing
matchable to cross-check, so the verdict is stored unbound. See Binding the
verdict to the finding’s citation.
The verdict is available only after the with block closes, since it is a
function of the whole span. Read obs.verdict after the block, not inside it.
A claim must be authored inside the scope and signed after it closes. Asserting
a claim while its grounding scope is still open would bind a verdict computed from
a partial observation, so it is refused.
A read only grounds when it matches the citation
GROUNDED is not “some loader returned data.” It is “a read that matches the
cited source returned data.” Reading a config file, a tokenizer, or a .env
through the same wrapped open() is an incidental read: it does not ground the
finding. The match is what makes UNGROUNDED mean the cited data did not
arrive, not merely that nothing was read.
Matching is by identifier (same absolute file path, same database target, same
scheme://host/path for a URL) or, when you opt in with content_address=True,
by the sha256: hash of the returned bytes against a cited data id.
When the observer cannot see: seams
The observed scope propagates into theasyncio tasks created inside it, but not
into a task that predates it, a library-spawned thread, or a child process. A
read on the far side of one of those boundaries is invisible. Rather than call
that invisible read a genuine absence, the observer records a seam and, when
the seam is relevant to the citation, returns OPAQUE:
- a thread start (
threading.Thread.start,_thread.start_new_thread), - an
asynciotask already pending when the scope opened, - a subprocess or new process (
subprocess,os.exec,os.fork, …), - a raw socket connection,
- a cited path opened through an uninstrumented reader (
os.open), aduckdbquery whose read path lives inside the SQL string, or a cited C-runtime file (HDF5, netCDF, Arrow) whose bytes emit no PEP-578 event and were not read through a wrapped reader.
OPAQUE on a file-cited finding: the silent-fallback UNGROUNDED tell survives
an unrelated network call. A socket seam does block a URL or content-address
citation, whose bytes can arrive over the network. Thread, subprocess, and
coverage-gap seams can hide anything, so they always block; an unknown seam or
citation kind blocks too (fail-closed). A cited URL with no observed HTTP read is
unknown coverage, so it is OPAQUE, never UNGROUNDED. An open of the cited
source that the observer watched fail, and that accounts for every open of
that path, hides nothing: it is recorded as a failed-open seam and the verdict
stays UNGROUNDED, naming the failure and its exception type. That is the
try/except silent fallback, reported as the tell it is.
Keep-alive HTTP is covered: pooled requests.Session, httpx.Client /
AsyncClient, and aiohttp sessions reused inside the scope are wrapped, so a
retrieval through one is a recorded read (or OPAQUE when the body streams),
never a false UNGROUNDED.
Honest bounds
The observer names what it cannot do:- For a plain
open()file,GROUNDEDmeans the cited file was opened for reading and is non-empty. Thesqliteandhttpwrappers observe the actual returned rows and bytes; the file path proxies flow by file size, so it does not prove the bytes were consumed. - A resource opened before the scope (a module-level or pooled connection
reused inside it) is not wrapped, so its reads are invisible and the finding can
read
UNGROUNDED. Open the cited source inside the scope for the tell to hold. - The verdict is tamper-evidence over what a cooperating producer’s run did. It is not a proof against an adversarial operator, who signs under their own key.
The causal oracle: an independent check
The observer measures flow: did the cited bytes arrive. The oracle measures influence: does the finding actually depend on the data. It perturbs the input, re-runs the pipeline, and sees whether the finding moves. It never reads the observer’s log, so a detector that agreed with itself cannot look correct here. The null is not yours to pick. A chosen null is a place to fish: choose the one a quantity is provably invariant to and the finding readsNOT_INFLUENCED however
honest it is. So the oracle derives the whole family of nulls from the input’s own
shape and routes the verdict on the profile across the family.
INFLUENCED needs every null to move the finding; NOT_INFLUENCED needs none of
them to. Anything in between is UNDECIDABLE, which is the honest reading of a
genuine mean: it is invariant under a reordering and moves when you zero the data,
and calling that hollow would accuse an honest computation. NOT_TESTED is the
oracle declining, with a typed reason, on a target it could not measure at all.
The oracle handles the honest hard case: a stochastic pipeline (an LLM at nonzero
temperature) moves run to run even with fixed input. It measures that run-to-run
noise first and calls INFLUENCED only when the perturbation moves the finding
past the noise floor. When the effect sits inside the noise band the answer is
UNDECIDABLE, never a silent INFLUENCED. Measuring the noise needs repeats: at
the default repeats=1 nothing is measured, so the floor is 0 and jitter alone
can clear it. The result records that as noise_measured=False and names it in
the reason, so a reader can tell a missing floor from a small one.
A prose finding needs a reduction to a scalar the oracle can compare.
numeric_extraction_reducer pulls the reported number out of an answer string
with no model, so the oracle stays a model-independent ground truth; a reducer
that runs a model (an embedding distance, an LLM judge) sets reinserts_model=True
and the result records it, so a measurement declares when its ground truth is no
longer model-free. Two controls tighten the noise call before it is made:
multiplicity=n widens the threshold when a finding is one of n (so the
noisiest of a family cannot cross the bar by chance), and thin_sigma_guard
widens it when the noise floor rests on too few repeats to trust. The thin-sigma
guard defaults off; the multiplicity widening is not off by default, because the
derived family’s own null count feeds it. Neither reaches a pipeline with no
measurable noise, where the threshold is a float-equality band instead of a
multiple of sigma, and multiplicity_applied on the result says which case you
got.
Flow and influence are different constructs, so reconcile reads a mismatch as a
construct difference, not a detector error. A finding can read the cited data
(flow) and then ignore it (no influence). The one combination worth investigating
is UNGROUNDED yet INFLUENCED: the data demonstrably shapes the finding but no
cited read was seen, which points at a coverage gap the observer missed.
The split over a pipeline
A single verdict answers one finding.summarize aggregates many into the numbers
a report states: the GROUNDED / UNGROUNDED / OPAQUE fractions, how often an
incidental read occurred that citation binding correctly refused to count, and
what fraction of the cited reads the observer actually saw.
OPAQUE dominates, the observer cannot see enough of the pipeline for the
other numbers to mean anything, so the honest response is to instrument deeper
before publishing a measurement.
When a run also records a per-finding effective-independence number,
summarize_independence reports the independence arm next to the split: what
fraction of findings rest on a single supporting line versus corroboration at two
or more, the fraction UNVERIFIABLE where the model lineage is soft, and the
same-model-collapse rate (corroborations a signer-only counter would call
independent that were one computed model counted twice). summarize_pilot folds
both arms into a slim natural-prevalence pilot and states the OPAQUE-coverage
bound: when the observer is mostly blind, the grounded prevalence is reported as
a lower bound, not a trustworthy number.
mareforma observe --doctor
reports what the observer covers in the current environment: which loaders are
wrapped, which seams force OPAQUE, and the standing bounds. mareforma measure
reads a run’s saved verdict receipts and prints the same split with OPAQUE
bucketed by seam kind, so a reviewer can rerun the number on their own pipeline.
Binding the verdict to the finding’s citation
AGROUNDED verdict is only as honest as the claim it is attached to. The verdict
is computed against the source you named in observe(cites=...), which need not be
the data the finding cites. So on assert_finding and submit_finding, at bind
time, the verdict’s cited set is cross-checked against the finding’s own citation:
its data_id set plus any data_source=. A GROUNDED whose cited set shares
nothing with the finding downgrades to OPAQUE with a signed reason, or raises
GroundingCitationMismatchError when you pass grounding_strict=True.
assert_claim has no citation to bind against. A plain claim carries no data_id
and no data_source=, and the free-text source_name never binds, so there is
nothing to cross-check the verdict with. Such a verdict is stored with
[no finding citation to bind] appended to its signed reason: the binding was not
exercised, not passed. Assert a finding when you want the verdict bound.
The modal honest workflow cites a path in the verdict, passes that same path as
data_source=, and content-addresses the data_id:
source_name never participates in binding. The check re-runs on
read as pure string comparison over stored identifiers, so a claim verified on a
different host, where the cited paths do not exist, is never false-flagged.
Bound into the claim
Pass the verdict toassert_finding or submit_finding and it is bound into the
signed in-toto statement, the append-only chain hash, and the queryable column,
so it is tamper-evident and re-checked on restore. The field is optional and
versioned: a claim asserted without the observer produces byte-identical signed
bytes to a pre-observer claim, and its absence is read as “no verdict recorded,”
never as tampering.
Grounding is a necessary floor for promotion, never sufficient. A finding that
execution shows is not grounded (UNGROUNDED or OPAQUE) never counts toward a
support-level promotion. A GROUNDED verdict still has to clear the
independent-signer counts described in Trust. A claim without a
verdict is unaffected, so the axis is purely additive.
Not the same as the declared classification
The observed axis is separate from the claim’s declaredclassification
(INFERRED / ANALYTICAL / DERIVED), which is what the producing agent asserts.
It is also separate from the soft grounding_score, a text-level hint that scores
a claim against its cited supports. Observed grounding is what execution shows.
The two never share a value space, so a reader can never confuse a
self-declaration with a computed result.
See the API reference for full signatures and
Findings for how a grounded finding earns its status.