Skip to main content
A finding cites a source. Nothing checks that the source was read. An agent can name /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 in observe(...). 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 the asyncio 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 asyncio task 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), a duckdb query 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.
Seam relevance keeps a seam from hiding a tell it could not have caused. A socket connection cannot deliver a local file read, so a socket seam does not force 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, GROUNDED means the cited file was opened for reading and is non-empty. The sqlite and http wrappers 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 reads NOT_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.
When 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.
Two commands make this inspectable from the shell. 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

A GROUNDED 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:
The free-text 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 to assert_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 declared classification (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.