Skip to main content
The authoritative changelog lives in CHANGELOG.md. This page mirrors it section for section from v0.3.1 on. v0.3.0 and the 0.2.x line are summarized here; read CHANGELOG.md for their full entries.

v0.3.13 - 2026-08-11

The influence oracle now picks its own nulls. Hand it the perturbation yourself and you can pick one the finding is provably invariant to, and read NOT_INFLUENCED however the pipeline works. The family is derived from the finding’s own data shape now, and the verdict routes on the profile across it.

Added

  • A derived null family. perturbation_oracle(perturb=None), the default, builds the nulls from the input’s shape: content-destroying ones (zeroed, constant) and marginal-preserving ones (permuted, reversed). A null identical to the base cannot perturb anything, so it is dropped and named in dropped_nulls rather than run.
  • The profile rule. INFLUENCED needs every null to move the finding, NOT_INFLUENCED needs none of them to, and anything between is UNDECIDABLE. That last case is the point: a genuine mean is invariant under a permutation, and calling it hollow for that would accuse an honest computation.
  • NOT_TESTED, with a typed reason. A shape with no family, a null that could not be built, a base run that failed, a null that crashed the target, a value the reducer could not reduce, and a value that came out NaN or infinite each read NOT_TESTED naming which. The three measurement numbers are None on such a row, so no reader takes a zero off it as a measurement.
  • The influence arm on mareforma measure. Per cited source, whether the finding depends on the data. mareforma audit runs its target once and never perturbs it, so every edge on its receipts reads NOT_TESTED: the report says influence was not tested rather than letting a grounding verdict stand in for an influence claim nobody measured.
  • A read says how many rows it held back. unverified_excluded and verify_excluded on the MCP pages, and read_unverified_exclusions on the graph. A project whose claims were written under a key nobody enrolled used to answer with an empty list and no way to tell that from an empty record.
  • mareforma.selfcheck ships in the wheel: four seeded failures a correct instrument must catch, runnable without the source tree.

Changed

  • diagnose and audit exit 3 on their own usage errors, not 2. This is a contract change. 2 is the code argparse uses for the same thing, so a gate reading 2 could not tell “you passed a non-Python target” from “your script rejected its arguments”.
  • A version marker is not a file extension. pipeline.v2 and model.v1.2 run; runspec.json.1 and app.log.1 stay refused.
  • claim list takes --limit and says when it capped. The flag is opt-in, so the JSON output is unchanged for anything already parsing it.
  • claim show marks an unbacked support level UNVERIFIED, as claim list already did. It is the command an auditor runs on the claim they suspect.

Fixed

  • A withheld row no longer truncates the record. has_more was computed after rows were held back, so hiding one row could report thirty-nine untouched claims as “the record ends here”, with no way to page past it.
  • A tampered claim no longer reaches a model as an ordinary row. A claim edited in the database file needs no SQL and fires no trigger, and the enumerating tools served it while the same server’s verify_claim called it tampered. It is withheld and counted now.
  • verify_claim no longer refuses the claims with the most evidence. The evidence ceiling gated the verdict instead of the trust map, so twenty-six labs on one proposition made every claim answer “unverifiable” about signatures that verify in 67 ms. The ceiling gates the map, which is the half that grows.
  • The forged-delimiter stripper no longer rewrites the field it checks. It returned NFKC-folded text, so 10⁻⁹ reached the model as 10−9 and 6×10²³ as 6×1023 while the verify surfaces, which read the raw row, still called the claim verified. Folding only detects now, and a field that needs it to reveal a delimiter is replaced whole rather than repaired.
  • Audit corpus resume binds a run record to its target. It verified a signature and read completed without checking the record belonged to the run being skipped, so copying a neighbour’s signed record skipped a sibling run without executing it.
  • A click.UsageError raised by the target is an aborted run that keeps its receipts, not a report that mareforma was invoked wrong. A target is allowed to be a click program.
  • The read ordering is served by an index. query() ordered by an expression no column index could serve, so every call scanned the table and sorted it under the process-wide lock. Measured over 2,000 claims at limit 20: 0.918 ms per call before, 0.009 ms after, and the cost no longer grows with the table.
  • A long-lived reader stops growing without bound. The health log took one line per read that dropped a row, and the skipped-line dedupe set kept every key it ever saw. Both are bounded, and the log still records a change of scale.

v0.3.12 - 2026-08-11

An agent can now read and audit a project over the Model Context Protocol, without being able to write to it. The rest of the release is the work that makes that safe, and the largest part of it is that verify no longer blesses what it did not check: a claim carrying no signature was reported verified, on the default path, in every release up to and including 0.3.11.

Added

  • A read-and-verify MCP server. mareforma mcp serve serves one project over the Model Context Protocol behind the optional mcp extra. Six tools: query_claims, search_claims, get_claim, proposition_status, trust_map, verify_claim. There is no write path and there will not be one: a claim asserted across a transport carries no observed grounding, and the record exists to hold claims to the grounding they earned. The project root is pinned once at startup rather than taken per request, and one graph is held for the process lifetime.
  • Claim text reaches a model as data, not as instructions. Every row the server returns is sanitized and wrapped in <untrusted_data> markers, and the server states that contract in its instructions so the reader knows what the markers mean. Claim text is written by whoever produced the claim, which on a shared record is usually another agent.
  • proposition_status reports question_status. consistent or divided names the state of the question, beside status for the state of the answer. Both derive from one frame computation, so they cannot disagree about whether a frame is contested.
  • An example that plants a silent failure and catches it. examples/07 runs two pipelines that print the same number, and shows only the one that read its data reading GROUNDED.

Changed

  • diagnose refuses a target that is not a Python program. A JSON run spec is a valid Python dict literal, so it used to compile, exit cleanly, read nothing, and draw UNGROUNDED with scope fully observed beside it: a false accusation carrying a false completeness claim. It is now a usage error, exit 2, before the target runs. audit already refused these; both commands make the same promise about what they watched, so both now enforce it. Anything runpy can run is untouched: .py, .pyw, .pyz, .pyc, .zip, a directory with a __main__, an extensionless script, and -m module.
  • The MCP page size is capped at 200 rows, and a capped page says so with a limit_capped_from field. The graph sizes its scan ceiling from the limit it is handed, so an unbounded limit was an unbounded scan holding the one lock every other tool call waits on. A short page that does not report the cap reads as “that is all there is”, which is a false answer about the record.
  • mareforma mcp serve refuses a project it cannot write to, and says which path to fix. The server signs nothing and writes no claims, but SQLite opens the graph read-write and journals beside it even on a pure read. A readable project that was not writable used to pass startup and then die with “attempt to write a readonly database”, which names no directory to fix.
  • mareforma.open() refuses two path mistakes that used to create a nested project: a path that resolves to an existing file, and a path named graph.db or sitting inside a .mareforma/ directory that is not already a project. Both raise ValueError. Pointing the library at the database file rather than the project directory is the mistake this catches.
  • mareforma status can read yellow on a project that meets every green condition. A stored project policy whose root signature no longer verifies now overlays the traffic light and appends its reason. The support-level breakdown also prints the retired-ladder notice.
  • JSONLDExporter requires classification and support_level on every row it is handed. It used to default them silently. First-party callers pass full list_claims rows and are unaffected; a caller passing a projected row to export(claims=...) now gets an error instead of a fabricated node.
  • mareforma mcp serve refuses to derive a target carrying more than 1000 evidence lines, configurable with --max-evidence-lines. Derivation cost is linear in that count and runs holding the one read lock every tool call shares, so an unbounded target stalled every other caller. The refusal names the count and the way to raise it. It refuses rather than answering over a subset, because a trust map derived over part of the evidence is a wrong trust map.

Fixed

  • verify no longer reports a claim verified when nothing was checked. A signature bundle whose signatures array is empty or malformed names no signer, so the pubkey check was skipped and the command fell through to exit 0. A bundle that names no signer is now refused. The bundle-file path already refused these; the stored-claim path now agrees with it.
  • verify no longer reports an unsigned claim as verified. A claim with no signature reached none of the signature checks, so the verdict fell through to verified and exit 0. It is now unverifiable, exit 2. This was reachable with no database access and no attacker: open a project, assert a claim, never run bootstrap. That is the path the quickstart teaches and the path examples/06_ci_verify gates CI on, so anyone following that example had a gate that never fired. Present in every release up to 0.3.11. The trust map had the right answer throughout and rendered attributability as unsigned; the verdict never read its own map.
  • The witnessing axis reports the record it found, not a proof it did not check. It read logged with the residual “signed and recorded in a transparency log with an inclusion proof” whenever a row existed in rekor_inclusions. Nothing opened the stored proof: the Merkle check runs at restore, and the table’s triggers block UPDATE and DELETE but permit INSERT. It now reads inclusion record present and says the proof is not re-checked on read.
  • The contestation axis reports the column it read. It stated that a signed contradiction verdict had marked a claim invalid, from t_invalid alone. No trigger guards that column and the signed verdicts are never replayed on read, so one direct write could fabricate a contradiction or erase a real one. The signal is now invalidation-recorded rather than signed-verdict.
  • verify no longer treats exit 0 as the fall-through. The verdict was matched against string literals, so any verdict the classifier grows that the branches did not name would have exited 0 and reported “verified”. Exit 0 is now the branch that has to be earned; anything else is unverifiable.
  • The trust map no longer reports a signature for a claim that has none. Re-verification was gated on a row carrying both an asserter keyid and a signature bundle, so a row with a keyid stapled on and no bundle skipped the check entirely and fell back to the stored verified gate, which passes PRELIMINARY rows through. The map read “signature re-verified on read” beside a keyid while verify called the same claim tampered. Enrolment is now read on the signer the bundle names, the same keyid the verification uses. Present since 0.3.11, reachable through mareforma map and graph.trust_map(); verify was correct throughout.
  • Short label fields can no longer forge a closing delimiter. source_name, generated_by and validated_by skip the <untrusted_data> wrapper because delimiters around a short label are noise, and they were skipping the forged-delimiter strip with it. A run token reading </untrusted_data> is serialised into the same object as the wrapped text and closes a delimiter it was never given. The labels are still not wrapped; they are now stripped, like every other string in the row. Present since 0.3.11 in query_for_llm.

Deprecated

  • The support ladder, whole. 0.4.0 removes the PRELIMINARY / REPLICATED / ESTABLISHED labels, the stored support_level column, the promotion machinery, and the query(min_support=...) filter. Read the trust map’s independence axis, or proposition_status(), for how much distinct backing a finding has. Both surfaces warn today: the retired module labels on attribute access, and min_support on the reads that filter by it. The filter is the path callers actually take, so a removal announced only through the module attribute would have reached nobody.
  • frame_status on proposition_status. It echoed the answer’s own status word. Read question_status instead. Removed in 0.4.0.
  • assert_claim(seed=True). It still writes a signed ESTABLISHED claim and remains the anchor that bootstraps a fresh trust chain. Removed in 0.4.0.

Migration

  • get_tools()’s query_graph no longer defaults min_support to "PRELIMINARY". That value is the floor of the ladder, so it filtered nothing, but it reached query as an explicit argument and made every agent tool call warn about a deprecation the caller had not asked for. Passing nothing now means asking for nothing; the rows returned are unchanged.
  • The verify and trust_map fixes change the answer only for rows that were already inconsistent with their own signatures. The three Changed entries above are the only other calls whose result moves: two open() paths that now refuse, and one exporter that now requires the fields it reads.

v0.3.11 - 2026-08-08

A hardening release. An adversarial audit of the substrate found defects on every path a claim travels, and this closes them. The theme is that a claim is now trusted for what can be checked about it, not for what it says about itself.

Added

  • Strict promotion is project state, not a caller’s flag. Opening a project with strict_promotion=True records a root-signed, one-way policy that every write path, the CLI and restore read. Before, the flag lived on the handle, so a second opener promoted the rows the strict opener had refused, and nothing recorded the bypass. Declaring the policy requires the root key; a keyless or non-root caller is refused with a typed error. The policy envelope carries a version, so envelopes signed before the field keep verifying.
  • A supports revision counter. graph.db records a counter bumped by every claim insert and every supports-edge change, and the cache records the value it was built from. A torn commit that loses the cache half of an in-place edge edit is now detected and rebuilt instead of serving pre-edit lineage indefinitely.
  • A wrap-invariant FASTA canonical form, and the rdkit SMILES form split from its no-rdkit fallback. One recorded form name used to mean different bytes on different hosts; the rdkit form now refuses without rdkit, the string form is registered under its own name, and a chem extra installs rdkit.
  • Retire a plan whose gates cannot run. graph.retire_plan(plan_id, alpha=, reason=) registers a replacement carrying the retired plan’s own rule at a usable alpha, records a signed retirement attestation, and re-gates the stranded evidence under the replacement. Only the alpha may move, so a repair cannot re-choose the side of the null once the numbers are known, and the retired row keeps its original values. A release before the (0, 0.5) bound could register a plan the gates cannot evaluate; its evidence lines dropped out of every count with no way to recover them.
  • A post_hoc flag on proposition_status. A count resting on a plan chosen after the estimates were in view, either a one-shot plan or the replacement a retirement resolved a stranded line to, now says so. A reader can tell a pre-registered gate from one picked once the numbers were known.
  • Publishing gates on the test suite, and the release job checks the packaged version against the tag it is publishing.
  • The trust map refuses to render when its engine version drifts from the package. Every map carries the engine version that computed it, so a stored record names the build behind its tiers. That stamp is now bound to the package version, and a build where the two disagree raises TrustMapVersionError instead of emitting a map. A map whose tiers came from one engine while naming another is worse than no map, because the name is what a later reader checks the tiers against.
  • The pre-publish suite fails an artifact missing the read-side lineage authentication. A wheel or sdist in dist/ built before independence keyed off the signed model lineage ships a trust layer a forged model_lineage column can inflate to CONVERGENT. Any such artifact now fails the suite rather than being published.

Breaking

  • Enrolling a key stops a project counting the claims it wrote before that key. A project can start unsigned, and its claims count on a run axis keyed on generated_by. The moment a validator enrols, that axis is withdrawn: generated_by is a value the writer picks, so once a project signs, a writer with database access can add an unsigned claim under any run token and the gate cannot tell it from an honest pre-key one. That closed two measured bypasses, stripping the signature off an existing claim and inserting a fabricated unsigned tree, both of which read as convergent support with nothing disclosed. The cost lands on an honest operator who enrolled late: those findings stay in the graph and stop counting, the proposition reads UNTESTED, lines_skipped rises, and each drop is named unregistered_signer_skipped in .mareforma/health.jsonl. There is no un-enrol. Open a project with its key from the first write, or keep it unsigned.
  • restore accepts a recovery it used to refuse, and reports it. A claim with no signature in a project that enrols a validator no longer fails the whole rebuild. One keyless write after enrolment, an unattended run or a collaborator without the key, used to make every later backup unrestorable, and nothing said so until recovery day. Such claims come back, the return value carries unsigned_in_signed_mode counting them, and a warning names the first few. They still count on no axis, so the read path and restore now describe the same graph. A claim carrying a statement_cid with no signature_bundle is still refused: it was signed once and the signature is gone, which is tampering rather than a keyless write.
  • A retirement resolves only behind a verified attestation. The read path re-derives a plan retirement from the signed record, so a graph carrying a hand-planted plan_retirements row stops resolving stranded lines through it. A row whose claim does not render the plan, replacement and reason it names resolves nothing, and restore refuses it.
  • Upgrade every writer on a project together. Once a writer opens a project with this release, an older release can still open it (the schema version is not bumped), but its promotion now trips the newer promotion guard and is left pending instead of landing. The older release used to swallow that failure and lose the promotion in silence. mareforma status now names the stuck count as convergence_retry_pending, and a writer on this release clears it. See the migration note below.
  • mareforma verify <claim-id> exits 2, not 0, for a signed claim whose signer is not an enrolled validator. Auditor-mode verification uses public material only, so a signer that never enrolled cannot be authenticated: the claim’s binding is all that could be checked, never its signature. The old exit 0 let a CI gate pass a 64-zero-byte signature under a keyid that was never enrolled. Exit 2 is the unverifiable verdict, not a failure: enrol the signer’s key and the same claim reaches a definite one. A CI gate that treats any non-zero exit as tampering will now fail on this, and should route 2 to “cannot tell yet.”
  • A signer that does not authenticate contributes no distinct-signer unit. A finding whose claim names a signer that is not an enrolled validator, or that carries no signature bundle at all, counts on no axis, where before an unenrolled or bundle-less signer still read as a distinct source. A project that leaned on unenrolled participants for its independence count sees those lines drop and disclosed as skipped; enroll the signer to count the line again.
  • Prediction no longer accepts preregistered. The constructor keyword is gone and to_dict() no longer carries the key; the store owns the flag. Delete the argument from any call site. The v0.3.10 reference documented it and the shipped compounding example used it, so copied code needs the edit.
  • Prediction refuses an alpha of 0.5 or above. The bound narrowed from (0, 1) to (0, 0.5), so a plan that constructed on the previous release now raises ValueError at construction. The gate is one-sided, and at an alpha of half or more it cannot separate the sides, which is the state a release before the bound could register and then fail to evaluate. Re-register any such plan at a usable alpha, or retire it with retire_plan, which exists for the plans already recorded above the bound.
  • Receipts written before this release no longer summarize. The grounding axis version moved, so summarize_receipts raises GroundingAxisMismatchError on any receipt from v0.3.9 or v0.3.10 and mareforma measure exits 1 on a stored receipts.jsonl. Re-run the observation to produce receipts on the current axis.
  • perturbation_oracle returns different verdicts for identical inputs. The effect size is now the largest single perturbation move rather than the pooled mean, the multiple-comparison family includes the perturbation count, and thin noise is reported at one repeat. An influence rate computed on v0.3.10 does not carry over.
  • mareforma verify reports a foreign-key bundle as unverifiable, not tampered. A bundle signed with a key other than the local one now exits 2 with verdict unverifiable, where it exited 1 with tampered. A CI gate keyed on the documented 1-versus-2 split takes the opposite branch. Pass --key to verify against the signing key.
  • mareforma reexec usage errors exit 3, not 2. A missing file or an unknown flag now returns the usage code rather than the could-not-re-execute code, so a gate that treated 2 as inconclusive-and-continue aborts on 3.
  • claim add writes to the nearest ancestor project. claim add, claim update, claim validate and validator add now join the project above the current directory instead of creating a new one in place. The same command run in a subdirectory writes to a different graph than it did before.
  • health.claims_contradicted counts a different population. It now counts claims marked invalid by a signed contradiction verdict, where it counted claims that assert a contradiction. Same name, same type, different number, so a dashboard or gate reading it silently changes meaning.
  • The exporters refuse a document rather than returning a partial one. JSON-LD, PROV-O and RO-Crate raise UnverifiedClaimError naming the claims when any row fails verification, and mareforma export exits 1. They also raise FileNotFoundError on a root with no graph instead of returning an empty document, as does export_bundle.build_statement.
  • The RO-Crate signature property is an object, not a string. It is now the parsed DSSE envelope where it was the envelope as a JSON string, so a consumer calling json.loads on it raises. A malformed bundle is omitted rather than passed through opaquely.
  • Agent identifiers in the RO-Crate and PROV-O exports are percent-encoded. A model name carrying a colon now renders as %3A where it rendered as _, so documents exported by different releases do not join on the agent node.
  • mareforma export --bundle --json exits 1. The combination was a documented no-op and is now refused as mutually exclusive, so a script passing a blanket --json to every export fails.
  • write_bundle refuses projects it used to sign. A signing key that is not the graph’s trust root, and any graph containing a claim with no asserter signature, are both refused with BundleExportError. Projects whose earliest claims predate their signing key are the common case.
  • register_canonicalizer refuses a duplicate name. Re-registering an existing form now raises; pass override=True to replace it deliberately. A module that registered a form and is then reloaded fails on the second import.
  • The JSON-LD export declares @version: 1.1. A JSON-LD 1.0 processor is required to reject the document. The context also maps evidence as a JSON literal, so it survives expansion where it was previously dropped.
  • mareforma validator list can exit 1. A read-only listing now fails when the validator rows do not chain back to a single self-signed root, which aborts a set -e script that only meant to list them.
  • mareforma audit --out replaces the whole directory. Envelopes from an earlier run in the same output directory are deleted before the new ones are written. The deleted artifacts are signed evidence, so point re-runs at a fresh directory to keep them.
  • partial is true for any non-zero exit. In diagnose --json, audit receipts and run records, a target that exits non-zero is now reported as a truncated observation, where the flag was set only when the target raised.
  • is_doi no longer trims whitespace and requires a stricter form. A padded or multi-token supports entry classifies as an external reference rather than a DOI, which moves it between predicates in the JSON-LD export. graph.classify_supports returns a different answer for unchanged stored data.
  • Declaring the strict-promotion policy requires the project’s root key. Opening with strict_promotion=True from a keyless or non-root caller raises ProjectPolicyError, where the flag was previously accepted on any handle. This is the enforcement half of the policy entry under Added.

Changed

  • verify_claim_signatures returns False for a row that carries a signer keyid but no signature bundle, instead of exempting it as legacy.
  • A finding’s verdict inputs are signed. The record it already carried, the proposition it addresses, the plan that gated it, the datasets behind it, its bearing, and a digest over its ordered estimate line set, is bound into the claim’s signed statement, and a verdict re-derives against that copy where it is read. A finding written before the record carries none and signs to byte-identical bytes, so every claim already on disk keeps verifying.
  • Altering or deleting an estimate, a contrast, or an evidence line is disclosed on read and refused on restore. The independence count enumerates from the signed finding and joins downward, so a removed row leaves the finding visible and its signed digest catches the gap, instead of erasing the whole finding from an inner join and reading a dropped refutation as consensus.
  • A rewritten proposition is caught where the verdict is read, not only on restore. A finding’s signed claim text is the rendering of the proposition it attests, so the live proposition row must render back to it; a mismatch drops the finding rather than counting evidence for a sentence the finding never made. Both renderings are normalised, so two agents naming one proposition with different capitalisation still read and restore without a false refusal.
  • A failed signature is distinct from an absent one. A finding signed by an enrolled validator whose signature no longer verifies is a disclosed skip and a restore refusal; a finding that never carried a signature keeps the previous fallback. Before, a swapped stored pubkey changed a verdict with the skip counter reading zero.
  • mareforma status cannot read green while a promoted claim fails re-verification, and it reports the count of promotions left pending by a swallowed convergence retry.
  • effect_estimates, contrasts, propositions and validators are append-only and undeletable, and predictions.plan_id, the primary key, joins its own trigger’s watch list. A direct in-place edit or delete of any of them is refused at the storage layer, the same guard findings and evidence_lines already carried.
  • claims.toml keeps a .prev copy on each write. A bad rewrite, or a graph lost along with its only backup, still leaves one recovery point, at constant cost and with no per-write signature work.
  • The detached bundle verifier gained the self-validation refusal restore already applied, so the live read, restore, and the bundle verifier agree on what a promotion needs; a test fails if the three rules diverge again.
  • list_claims accepts a limit, so a caller can bound the verify-on-read work on a large graph the way query and search already do.
  • effective_independence reports lines_skipped, so the independence axis does not read a confident number off a line set that silently lost lines.
  • Omitting generated_by no longer exempts a submit from the pre-registration gate. The write resolves it to the default run token and the gate asks about that same token, so a project that never sets a run token raises PostHocPlanError on a preregistered=1 plan once any finding exists under the default. Register the plan before the run executes, or submit under a fresh run token.
  • A failed open of a cited source no longer floors the verdict to OPAQUE. When the observed failures account for every open of the cited path, the open provably returned no file object and nothing is left unexplained, so the scope lands UNGROUNDED with the failure and its exception type named in the reason. This reverses the 0.3.10 sentence below, which said a failed open still floors to OPAQUE. One open more than the observed failures still lands OPAQUE on the hidden-reader gap.
  • A support level above PRELIMINARY is served as verified only when the signed evidence behind it verifies too: a signature-checked replication verdict naming the claim, or distinct-signer convergence on a shared anchor. A row whose level has no such backing reads verified=False and stays out of gated queries. The live read path and restore share one derivation, so they cannot drift apart.
  • The project policy records when each rule was first declared, so extending it with a second rule leaves the first rule’s window where it was. A policy envelope signed before this release still verifies and keeps its own date.
  • The export bundle is described as what it emits, an in-toto Statement v1 in a DSSE envelope. Earlier releases called it SCITT-style; SCITT names a COSE signed statement, which this package does not produce.
  • The trust map drops the topology flag no caller could act on, and TRUST_MAP_VERSION is stamped for the new shape.
  • compute_health no longer takes the project root it never used.
  • verify_rekor_inclusion takes the signed claim envelope as a third argument, so a call written against 0.3.10 raises TypeError until it passes the envelope the entry is meant to witness.
  • The exceptions the API reference names are exported at the top level.
  • Dead surfaces are gone: the selective-wrapping selectors, the unreachable telemetry writer, the unread role-attestation sidecar, an unauthenticated model key helper, and a root identity parameter no caller could set. The rich dependency was never imported and is dropped.
  • All four empty extras are gone: clawinstitute, tooluniverse, gemini and docs. Every adapter runs on core dependencies, so an extra per adapter installed nothing and still succeeded, reading as “already satisfied” to anyone who ran it. Adapters are opt-in by import.

Fixed

  • restore reports a malformed trust-layer row instead of crashing. TOML can hold arrays, inline tables, local times and integers wider than SQLite takes, and a hand-edited backup carrying one raised ProgrammingError, or OverflowError, straight out of restore. Every trust table now rejects a non-scalar with RestoreError(kind='trust_row_rejected') naming the row and the column, which is the documented error surface a recovery script catches.
  • Verify-on-read binds a signed claim’s stored fields to its envelope. Rewritten text, a copied bundle, a removed bundle and a forged signer keyid are all refused at both gated levels instead of being served as verified, and the append-only triggers watch the bundle and the keyid so the rewrite is refused at the storage layer first.
  • A single statement can no longer forge a grounding verdict or promote a claim.
  • Every input a trust gate counts is re-derived where it is read. The read path and restore share one verifier, so a finding’s plan is checked against the plan its own claim recorded, and a plan’s rule columns against the identifier that keys them. A direct-SQL rewrite that would reflip a bearing drops the line and discloses it instead of moving the count in silence, and restore refuses the recovery outright.
  • findings and evidence_lines are append-only and undeletable. A finding’s plan and bearing, and a line’s data and model lineage, gate every count above them, so an in-place edit is refused at the storage layer.
  • retire_plan refuses two repairs that used to succeed quietly: one that recovers no evidence line at all, and one whose replacement plan already exists, which would drop the disclosure the recovered count depends on.
  • A one-shot finding’s synthesised plan is held to the same (0, 0.5) alpha bound as a registered one, so no write path mints a plan the gates cannot evaluate.
  • Restore verifies what it replays: a second self-signed root in claims.toml cannot void the trust layer, an unsigned finding edge cannot replay as an independent line, the durable promotion gates are re-applied to rebuilt rows, and an incomplete Rekor sidecar entry is refused.
  • Independence counts only what can be authenticated. Withdrawn and invalidated claims stop contributing, a null lineage column falls back to the signed record rather than skipping re-authentication, and a defaulted validator type no longer certifies an independent line.
  • The observer fails closed. The HTTP transport allowlist refuses an unknown transport, a nested scope replays its evidence into its parent instead of blinding it, reads are recorded under absolute urls with credentials stripped, and an unreadable response floors to opaque.
  • A tool adapter signs only what it observed. Environment facts it did not watch are refused, and callee cache metadata no longer writes a supports edge.
  • Read-only commands stay read-only: verify and map no longer enroll the caller’s key as the root validator, and an audited child process cannot reach the corpus signing key.
  • Every read surface verifies a high-trust row, including list_claims, so the JSON-LD, PROV-O and RO-Crate exporters and mareforma claim list no longer publish a REPLICATED or ESTABLISHED row whose signature does not re-verify.
  • An enrolled validator is bound to its own key: a row whose keyid is not the public key id of its own pubkey_pem is refused, so one key cannot hold two identities on the independence axis.
  • Two paths that grounded a cited source without reading it are closed: an in-process transport that answers without a socket, and a database connection that creates the file it claims to read. The boundary stays the process rather than the caller’s honesty inside it, because the scope recorder is reachable from the observer’s own exports, so code in the same interpreter can still have a verdict minted for a read that did not happen.
  • An audit receipt attests what the observer recorded, not what the run directory held afterwards, so an audited target cannot rewrite its own verdict into the auditor’s signature.
  • Transactions end where they start. A refused delete releases its transaction instead of discarding later writes, and the Rekor sidecar write stays inside the caller’s transaction.
  • Reads are bounded and serialized: a read truncated by the scan ceiling is refused rather than served short, unbounded IN-lists bind as one json array, the acyclicity walk runs in sql, and reads and close run under the graph lock.
  • An export bundle is refused unless the trust root signed it, bundle verification checks the validation envelope’s signer rather than its label, and an exported artifact lands through an atomic replace so a failed write cannot destroy the previous one.
  • A Rekor inclusion proof is bound to the claim it witnesses. verify_rekor_inclusion refuses a proof whose proven body records another entry’s payload hash or signature, so a valid proof over some other claim no longer verifies this one.
  • The insecure-Rekor flag reaches submit and fetch instead of being read once and dropped, and the log-pubkey pin is written durably and checked when it is read.
  • open() resolves the project root once, so changing directory under a live graph cannot split the signed corpus across two trees.
  • The documentation describes the shipped behavior. Every open() keyword, the full oracle and assert_claim signatures, six undocumented graph methods, the refusals validate() can raise, the restore signature and its error kinds, and six CLI commands that had no section are now in the reference, and the quickstart follows a path that reaches the established level.
  • The example walkthroughs print what a reader’s own run prints. Two transcripts carried a claim id and a validation date from the run that recorded them, values no other run reproduces, and both are now elided the way the rest of the transcripts already were.
  • The shipped sdist runs its own suite: the guards that read repository files skip there instead of failing where those files do not exist.

Migration

Upgrade every writer on a project to this release together. The schema version is not bumped, so a project stays openable in both directions, and a graph this release writes still reads under the older one. What changes is promotion: a convergence check run by the older release now trips the newer promotion guard and leaves the claim below the level its evidence earns, flagged for retry rather than lost. The older release swallowed that failure silently. Run mareforma status to see the pending count (convergence_retry_pending); a writer on this release re-runs the check and clears it. The retry has to come from this release: the older one reports the count in health() but its own refresh_convergence() promotes nothing and leaves the count where it was. A human validation is the harder case, and it is not queued. validate() called from the older release on an upgraded project raises mareforma:append_only:promotion_unmarked, the transaction rolls back whole, and the claim keeps its level with validated_by still empty and nothing added to the retry count. The validation is lost rather than deferred, and the message the older release prints is the internal sentinel with no remedy attached. Re-run the validation from this release. No data migration, no reindex, and no re-sign: the only action is to move the remaining writers onto this release.

v0.3.10 - 2026-07-16

Added

  • Model and method lineage on the evidence line. The observer records which model and method authored a finding, computed from the request the producer sent. Tiered COMPUTED (a body-parse at a recognized provider host), PROXY (a producer declaration), or UNVERIFIABLE (a fine-tune or wrapper whose base is not declarable). Corroboration counts distinct models, and a human check is the strongest independent source.
  • Local models earn content-addressed lineage. A call to a local inference server is COMPUTED via a weights-digest attestor: the served weights’ sha256 is the model’s distinctness key, so two local models are told apart by their weights, not a self-chosen name.
  • Wider execution observation. Grounding and lineage fire on the idioms real pipelines use: reads through io.open (pathlib), model calls at httpx send and the aiohttp seam (the provider SDK and litellm paths), and a cited polars read grounds. A grounded read and a computed lineage are gated on a successful response, so an error body never grounds a cited URL, and a duckdb query floors to OPAQUE rather than a false UNGROUNDED.
  • mareforma audit. A post-hoc auditor that signs one grounding receipt per finding, verifiable without the producer.
  • Re-execution faithfulness proxy. Re-runs a recorded step and reports whether the result reproduces, placed on the trust map.
  • Effective independence arm. The measurement reports the effective-independence distribution and the same-model-collapse rate, with a causal oracle prose path and a pilot that states its OPAQUE-coverage bound.
  • Pre-registration gate. A plan registered after the run’s first execution is refused, so a rule cannot be back-dated to a run it did not precede.

Deprecated

  • The public REPLICATED and ESTABLISHED labels. They read as settled conclusions the substrate does not compute. The internal support levels are unchanged; only the public module attributes warn.

Removed

  • The GRADE evidence vector, literature ingest (ingest, ask, narrative), DOI network resolution (the regex-only is_doi helper stays), and activity hooks. Existing graphs open unchanged.

Changed

  • The top status label CORROBORATED is renamed CONVERGENT. The state and its rule are unchanged: two or more independent-lineage supporting lines, none refuting. The word changes because it over-claimed. Distinct-model is necessary, not sufficient, for independence. CONVERGENT states the structural fact that lineage-distinct lines converge and names cross-model error correlation as the residual, rather than reading as a corroboration or independence verdict. Reading Status("CORROBORATED") or Status.CORROBORATED still resolves to Status.CONVERGENT this release and emits a DeprecationWarning; a future release removes the alias. Status is recomputed on read and never stored, so there is no stored-value migration. The policy stamp moves from status_policy@v3 to status_policy@v4.

Security

  • Model lineage is bound into the signed finding and authenticated on read. The lineage that drives independence was a denormalized column a writer could forge; it is now part of the signed payload and the independence read checks it, so a forged column no longer moves the count. Legacy unbound findings read as UNVERIFIABLE rather than being rejected.
  • A producer-controlled transport is classified PROXY, not COMPUTED, and an agreeing producer declaration can no longer pull a seam-verified COMPUTED model down. Absent model lineage reads as UNVERIFIABLE, never as a confident independent line.
  • The local weights probe accepts only content-addressed digests. Other local servers ship Ollama-compatible surfaces whose digest is a constant sentinel or the sha256 of the model name; either would have minted a COMPUTED weights-digest lineage off a fabricated identity, and the fake digest would have scored as a distinct model and forged cross-model independence. The probe now requires a well-formed sha256 payload that is not the hash of the model’s own name, and fails closed to no digest, so the call stays UNVERIFIABLE.
  • Single-operator topology is named on the independence axis. When every signer traces to one trust root the count says so and rests on distinct-model or human lines within that domain.
  • Replication promotion is gated on grounding and a distinct signer, and the invalidation gate is re-asserted on the validation write, so an invalidated or ungrounded claim cannot promote past a concurrent contradiction.
  • Rekor URL validation runs at submit entry like the fetch paths, and rejects non-decimal and IPv6-embedded SSRF host forms.
  • Key rotation writes durably. The rotated key is written to an unpredictable temp, fsynced before the rename, so a crash cannot leave a zero-byte key and two rotations cannot clobber a shared temp.

Fixed

  • Backup no longer crashes on a null verdict or Rekor field, restore round-trips the finding evidence tree and validates section shapes, and a corrupt graph opens as a typed DatabaseError.
  • The supports cache is maintained when a claim’s supports are edited or the claim is deleted, so query_provenance stops serving pre-edit or dangling lineage.
  • Graph mutations serialize across threads, update_claim applies the same write invariants as add_claim, and an established seed stays out of its own convergence promotion.
  • A read grounds only on a 2xx response, so an error body or redirect never grounds a cited URL or mints lineage.
  • mareforma verify reads a signed audit run.json as unverifiable (trusted via resume), not as tampered.
  • CLI: export and activity discover the enclosing project from a subdirectory, a wrong-key audit receipt reads as unverifiable, a reexec map lookup failure exits as a usage error, and the no-project hint points at a real command.
  • The CLI reference documents audit and reexec, and a drift guard fails when any shipped command leaves the reference. TRUST_MAP_VERSION moves to v0.3.10.

Performance

  • The findings table carries a claim_id index the write path and trust map need, convergence peer lookup no longer full-scans on every insert, per-frame independence counts and per-scope reads are memoized, and the local Ollama weights-digest probe is cached per server and model.

Packaging

  • The setuptools build floor is raised for the string license metadata, and the test-heavy extra is synced with the loaders it exercises.

v0.3.9 - 2026-07-08

Three passes that make trust honest and legible. The grounding verdict now binds to the data a finding actually read. The trust a claim carries is read as a structured map instead of one word, with three commands that make it legible to a stranger in a minute. And the open-issue burn-down closes, with the core-derived classification engine removed. Additive on the schema. See Grounding and Trust.

Added

  • Verdict-to-citation binding. An observed GROUNDED verdict now proves it attests the finding’s own data. The sources a read was actually observed for are carried into the signed record (grounding axis v0.3.9) and cross-checked against the finding’s citation at write time and again on read. A producer who cites a dataset but reads only an incidental decoy no longer earns GROUNDED: a disjoint GROUNDED downgrades to OPAQUE with a signed reason and a grounding_citation_mismatch health event, or raises under grounding_strict=True. Evidence lines gain data_source= so citing a path and content-addressing the data_id binds.
  • The per-finding trust map. graph.trust_map(claim) and mareforma map <claim> place every trust property (attributability, provenance, grounding, methodological validity, leakage, independence, contestation, standing, trust-root, witnessing) at its tier (COMPUTED / PROXIED / DEFERRED) with the residual named. A property Mareforma cannot observe is stated, never inferred. --json emits a canonicalizable record; --html writes one self-contained page.
  • mareforma verify <target>. An audit receipt at the moment of the check: it re-verifies signatures, the grounding-to-citation binding, and support level, then prints the trust map. It detects its target by shape (claim id, signed bundle, or export directory) and verifies a claim from public material alone. Exit codes are stable for CI (0 verified, 1 tamper or binding violation, 2 unverifiable, 3 usage error), so a bad flag never masquerades as a verdict. --json emits the verdict for a gate to parse.
  • mareforma diagnose -- <cmd>. Runs a Python target in-process under the grounding observer and reports the reads, seams, and coverage it saw. With --cites it also computes the grounding verdict; without one it reports observation only and never guesses a citation. A crashing target still prints its partial observation and exits with the target’s own code.
  • mareforma observe --doctor and mareforma measure. --doctor self-reports which loaders are wrapped and which seams force OPAQUE in the current environment. measure aggregates a run’s verdicts into the reported split, OPAQUE bucketed by seam kind. --redact-home rewrites $HOME in emitted artifacts, never in a signed receipt.
  • Keep-alive HTTP and C-extension coverage. Pooled requests.Session, httpx.Client / AsyncClient, and aiohttp sessions, and the C-runtime readers (h5py, pyarrow, netCDF4), are wrapped only-if-imported, so a retrieval or an HDF5 / netCDF / Arrow read reaches GROUNDED instead of a false UNGROUNDED. A loader imported inside an open scope is wrapped too.
  • The independence axis. The trust map reports independence separately from the support ladder, marking it UNVERIFIABLE whenever fewer than two trust roots are enrolled, the honest reading when one operator could mint every key.
  • strict_promotion option. mareforma.open(strict_promotion=True) gates REPLICATED on non-NULL data on both sides of a converging pair. Off by default; it only ever adds the requirement.
  • Declared metric reducer for the causal oracle. declared_reducer(...) names the reduction a prose finding needs and records whether it reinserts a model into the ground truth. (The oracle itself shipped in v0.3.8.)

Changed

  • UNGROUNDED means genuine absence, not blindness. A seam blocks the UNGROUNDED verdict only when it could have hidden a read of a citation kind actually present. A socket seam no longer forces OPAQUE on a file-cited finding, while URL and content-address citations stay blocked. Thread, subprocess, and coverage-gap seams block everything; unknown kinds fail closed.
  • REPLICATED is a convergence marker, not a claim of independence. The README and trust docs stop leading with it: signing keys are operator-mintable, so distinct signatures are a weak prior. Independence lives on its own axis.
  • The claim-recording agent tool is renamed record_claim. It was assert_finding, which shadowed the EpistemicGraph.assert_finding method. A deprecated assert_finding alias is available for one release via get_tools(include_deprecated_aliases=True) and warns on use.
  • The cycle check runs as one recursive query instead of one per ancestor, and an oversized reachable graph raises a distinct GraphTooLargeError rather than a false “cycle.”
  • RO-Crate and PROV-O exports align with the profile shapes. The RO-Crate root entity carries a license and a non-null datePublished and separates data entities (hasPart) from provenance actions (mentions); PROV-O labels use rdfs:label. The tests check shape and label vocabulary, not full validator conformance.
  • mareforma verify subsumes the old bundle-path command. The prior mareforma verify <bundle> invocation keeps working as the file-detection case; a missing local key exits 2 (unverifiable) rather than 1.
  • The per-connection validator chain-verification cache now persists, so a repeated enrollment check skips the chain walk.

Fixed

  • Re-ingesting a paper no longer leaves orphaned full-text-search rows, even when the re-extraction is empty. The ingest path deletes a document’s prior claims by document id before inserting the fresh set, so the FTS delete trigger fires.
  • Multi-role signatures are re-verified on the live read path, so a forged role attestation is caught on read and by mareforma verify, not only at restore.
  • Deleting a signed claim raises the typed SignedClaimImmutableError for both delete_claim and delete_claims_by_generated_by.
  • The API-version probe rejects a neighbouring major. The clawinstitute check matched “v10” and “v1beta2” against “v1”; it now matches the exact major or a minor under it.
  • independence_counts stops full-scanning effect_estimates, via an index on contrasts(line_id).
  • The sdist ships a complete, runnable test suite (conftest, shared helpers, and every test subpackage).
  • The Dependabot config no longer advertises a lockfile the repo does not commit.
  • The reference docs match the code. The status-policy stamp, the export --format choices, and the default-format PROV-O scope note are corrected.

Removed

  • The core-derived classification engine. mareforma.derivation (the keyword and log-template classifier), its [derivation] install extra, and the tree_sitter dependencies are gone; execution-observed grounding computes the same signal from observed reads.
  • The dead [git] install extra and the unused gitpython dev dependency.

v0.3.8 - 2026-07-06

Execution-observed grounding, and trust-layer hardening across every path a finding travels: read, convergence, backup, restore, and the export bundle. A bundle’s claims are checked against their own asserters and earned support level, restore rebuilds trust state from verifiable material rather than agent-set flags, and a project can require transparency-log witnessing before its findings converge. Additive on the schema (one new nullable column and one additive table, no migration). See Grounding and Trust.

Added

  • Execution-observed grounding. An optional observer records whether real data actually flowed into a finding (GROUNDED / UNGROUNDED / OPAQUE); the verdict is bound into the signed claim and re-derived on restore.
  • Required Rekor witnessing. graph.require_rekor_witnessing() writes a root-signed, one-way project policy; restore(enforce_rekor_policy=True) reconstructs convergence-eligibility only for claims carrying a verified, claim-bound inclusion proof.
  • Per-claim verification in the export bundle. verify_bundle checks each claim’s asserter signature bound to its content, the validator chain to a single root, and the displayed support level. The bundle signs over DSSE PAE, so it verifies with standard tooling.

Changed

  • search() re-verifies high-trust rows on read, matching query().
  • Convergence respects signed contradiction verdicts; a stranded re-check is retried.
  • claims.toml is written atomically and can be batched with graph.defer_backup().
  • Restore reconstructs the trust layer; witnessed state comes from the inclusion sidecar, not an unsigned field.
  • schema() describes the distinct-signer convergence rule (pinned by a test).

Fixed

  • The publish workflow fails when the release tag and built version disagree.
  • Read-only CLI commands discover an existing project instead of creating a stray one.
  • Export to a path outside the project reports success, not a false failure.
  • Root auto-enrollment is announced on stderr, not only a filterable warning.
  • A naive DOI cache timestamp is treated as UTC rather than crashing the write path.
  • mareforma ingest --help now documents the required TITLE / DOI / CLAIMS layout, and a structured-mode file that yields no claims exits non-zero and names the expected layout, instead of a silent success on an unparseable file.

v0.3.7 - 2026-06-30

Verified independence. REPLICATED now keys on the signing key, not a free-text generated_by string: two claims converge only when distinct keys sign them. And high-trust rows are re-verified on read, so a forged REPLICATED or ESTABLISHED row in a shared graph.db is caught at query time. Additive on the schema (one new nullable column, no migration); existing REPLICATED rows are grandfathered. See Trust.

Added

  • Verify on read: get_claim, query_claims, and query_provenance re-verify a REPLICATED or ESTABLISHED row’s signatures before serving it. query_* excludes a row whose signature does not verify; get_claim flags it verified=False. Neither raises. The check binds the signed payload to the row.
  • single_trust_domain disclosure on each ESTABLISHED row in query results and the exported bundle: whether every validator traces to one root of trust. A disclosure, not a Sybil guard.
  • asserter_keyid column, denormalized from the signature bundle and read by both the promotion query and the trust-layer count. Added to existing graphs by ALTER TABLE ADD COLUMN on first open.

Changed

  • REPLICATED keys on the signing key, not generated_by. Two claims sharing an ESTABLISHED upstream converge only when distinct keys sign them. generated_by becomes a display label. Existing REPLICATED rows are grandfathered with a legacy_promotion health event. A convergence signal, not proof: distinct keys do not prove the data is independent.
  • Status independence keys on the signing key (the same axis as promotion), with a data_id guard; unsigned lines fall back to the retired distinct-generated_by run axis. Policy moves from status_policy@v2 to status_policy@v3, recomputed on read.
  • artifact_hash is a collapse check, not a match requirement: equal data collapses two peers to one line, distinct data counts as two, absent data never blocks. This reverses the prior rule.
  • data_id is content-addressed (sha256: prefix) when the finding supplies the dataset bytes, so equal data collapses and an agent cannot fabricate distinctness.

v0.3.6 - 2026-06-17

The multi-line evidence tree. A finding can now carry several evidence lines instead of one, and Status counts independence by distinct run rather than distinct dataset. Additive on the schema (stays at v1, no migration). The single-line API is unchanged, and so are its Status outcomes for findings from distinct runs. See Findings.

Added

  • submit_finding / assert_finding take a lines=[EvidenceLine, ...] argument in place of the single estimate + data_id pair, recording several datasets or arms under one proposition and prediction. A finding with no lines raises ValueError; a finding where any line fails the gate rolls back whole. The finding’s identity is its full data_id set. The return dict gains bearings, the per-line bearing list.
  • Per-line bearing: each line’s bearing is recomputed on read, so a multi-line finding whose lines disagree reads as CONTESTED.

Changed

  • Status independence is now run-distinct: support and refute count distinct generated_by (run) with a data_id guard. One run contributes at most one support and one refute, so a single run cannot reach CORROBORATED on its own. Two findings on one proposition from the same run that previously read CORROBORATED now read PRELIMINARY; findings from distinct runs are unaffected. Policy stamp moves to status_policy@v2, recomputed on read with no migration.
  • A blank or whitespace generated_by is rejected at the finding write; a missing or default token writes but emits a health event.

v0.3.5 - 2026-06-15

The pre-registration split. v0.3.4 shipped the trust layer as a single assert_finding call. v0.3.5 separates the two earned steps of the hypothetico-deductive method: register the decision rule before the numbers are seen, then submit the outcome against it. The plan → finding edge becomes cryptographic. Additive only: no schema migration, schema stays at v1, and assert_finding / assert_claim keep working unchanged. See Findings.

Added

  • EpistemicGraph.register_plan(proposition, prediction): pre-register a decision rule. Writes the predictions row (preregistered=1) and its own signed plan attestation claim under idempotency key plan:{plan_id}, Rekor-anchorable like any other claim. Idempotent. Returns the content-addressed plan_id.
  • EpistemicGraph.submit_finding(proposition, prediction, estimate, *, data_id, ...): submit a finding against an already-registered plan. Requires the plan to exist (NoRegisteredPlanError), computes the bearing, and writes the finding’s signed claim whose supports[] cites the plan attestation, so the plan → finding edge is signed, not denormalised metadata. A finding already recorded for (content_id, data_id) under a different plan_id raises FindingPlanForkError.
  • mareforma.trust errors NoRegisteredPlanError and FindingPlanForkError.
  • mareforma.trust gates chain: Gate, gates_for(prediction), and evaluate_gates(estimate, gates) re-express the decision rule as an ordered short-circuit chain over the existing prediction columns. A one-element chain is bearing-identical to compute_bearing. Pure Python, no new schema column.

Changed

  • assert_finding now composes register_plan + submit_finding internally. Its synthesised plan is flagged preregistered=0, so a genuine up-front pre-registration stays distinguishable from a one-shot. Return shape, idempotency on (content_id, data_id), atomicity, and derived status are unchanged.
  • register_plan and submit_finding emit to the health/activity log.

Notes

  • Float determinism. The gate compares floats (p_value, CI bounds, alpha), but that is not a cross-host hazard: each IEEE-754 primitive op is a single correctly-rounded result. Divergence risk comes from accumulated computation (the pooling a meta-analysis does), which v0.3.5 does not perform. The abs(ci_level - expected_level) > 1e-9 check is a float-equality guard on caller input, not a status-driving reduction: it never softens or flips a bearing.
  • Single-line evidence model only. Multi-line evidence trees, per-line bearing, pooling / I2 / tau2, GRADE certainty, and the deferred gate regimes (multiplicity, magnitude bands, non-inferiority, dose-response, Bayesian) are not in this release.

v0.3.4 - 2026-06-11

The trust layer: structured findings with a computed bearing and a derived status. A free-text claim becomes a content-addressed proposition bound to a pre-registered prediction; the direction of evidence is computed from the registered rule and the result, never self-declared; and a count over independent data derives the status. Additive only: six new tables, schema stays at v1, and every finding still rides a signed claim as its attestation. See Findings.

Added

mareforma.trust
  • Proposition: a content-addressed, falsifiable claim. content_id is the answer (subject, relation, object, scope, direction, magnitude); frame_id is the question (direction and magnitude dropped). The same truth conditions collapse to one node across hosts and languages.
  • Prediction: a pre-registered decision rule. Superiority and equivalence (TOST) gates.
  • EffectEstimate / EvidenceLine / Contrast: the one-line evidence tree with metafor-named effect fields; rejects inconsistent input.
  • compute_bearing: the gate. Returns supports / refutes / neutral, computed rather than declared.
  • compute_status / compute_frame_status: the count-based status (UNTESTED, PRELIMINARY, CORROBORATED, REFUTED, CONTESTED) over independent data, versioned as status_policy@v1.
EpistemicGraph trust methods: register_proposition, assert_finding, proposition_status, get_proposition, query_frame. assert_finding validates, computes the bearing, writes a signed claim, persists the evidence tree, and derives the status in one call; idempotent on (content_id, data_id). Schema: six additive tables (propositions, predictions, findings, evidence_lines, contrasts, effect_estimates), prediction table append-only. Schema stays at v1; an existing v0.3.3 graph.db gains them on next open_db().

Notes

  • The superiority gate is one-sided at alpha. A supplied p-value is read as two-sided (the metafor/escalc convention), so significance is p < 2*alpha, matching the (1 - 2*alpha) confidence-interval path.

v0.3.3 - 2026-05-29

Adapter framework and substrate primitives. Five new primitives in core (events, tools, canonicalize, derivation, hooks) plus three opt-in adapters under mareforma.adapters.* and a literature-ingest CLI. Schema stays at v1; existing v0.3.2 graph.db auto-applies the new literature_claims and agent_activities tables on next open_db().

Added

Substrate primitives
  • mareforma.events: EventSource / EventHandler Protocols, typed EventPayload and ClaimResult, source-name constants (SOURCE_CLAWINSTITUTE, SOURCE_TOOLUNIVERSE, SOURCE_GEMINI, SOURCE_CLAUDE_CODE_PRETOOLUSE) so adapters dispatch on constants, not string literals.
  • mareforma.tools: Tool Protocol (name, version, call(**kwargs) -> ToolResult), ToolResult TypedDict, ReplayResult dataclass. The structural contract any wrappable callable satisfies.
  • mareforma.canonicalize: registry-based canonicalizer surface for adapter authors. Default json-c14n-v1 (RFC 8785 JCS) plus dsse-jcs-nfc-v1 (same bytes the signed-envelope layer produces). Importing mareforma.canonicalize registers rdkit-canonical-smiles-v1, fasta-nfc-v1, pdb-atom-sorted-v1 via the specialty submodule.
  • mareforma.derivation: substrate-derived classification. Deterministically derives ANALYTICAL vs INFERRED from a static source-code profile plus dynamic log templates (Drain parser). Source-profile extraction requires the [derivation] extra (tree_sitter); log-template extraction is pure stdlib.
  • mareforma.hooks: Claude Code PreToolUse handler (python -m mareforma.hooks) records every tool invocation as a prov:Activity row. agent_activities table is part of the canonical schema.
Capability-shaped predicate URI constants on mareforma.predicate_types (re-exported at the top level): TOOL_CALL_V1, CONTAINER_EXEC_V1, CODE_VARIATION_V1, HYPOTHESIS_V1, LITERATURE_INSIGHT_V1, SCIENCE_SKILL_V1, META_CLAIM_V1, WORKSHOP_EVENT_V1. Adapters import the constants: a typo on a constant name fails at import; a typo on a URI string would silently mis-classify a claim. Three opt-in adapters under mareforma.adapters.*:
  • mareforma.adapters.clawinstitute: generic ClawInstitute workshop-event hook. EventHook implements the EventSource Protocol; HttpxClient uses a pooled httpx.Client with follow_redirects=False and URL-quotes path segments. Eight typed exceptions share ClawInstituteApiError as parent. Untrusted workshop content runs through three sanitisation layers (raw-byte cap → sanitize_for_llmwrap_untrusted). Handler exceptions during dispatch() are caught and returned as ClaimResult(error=…) so a misbehaving subscriber cannot block peers.
  • mareforma.adapters.tooluniverse: wrap any mareforma.tools.Tool so each .call(**kwargs) records a signed tool-call:v1 claim with arguments digest, result digest, tool config fingerprint, timing. Container-exec class tools route to container-exec:v1. Over-cap results raise ResultTooLargeError.
  • mareforma.adapters.gemini: read-only ingest for Gemini for Science outputs (4 capabilities: code-variation, hypothesis, literature-insight, science-skill). Per-capability REQUIRED_FIELDS validation runs before assert_claim; string payload values flow through sanitize_for_llm; reserved keys (predicate_type, capability) are adapter-owned.
Literature ingest CLI: mareforma ingest <file>, mareforma ask "<query>", mareforma narrative. Paper claim drafts live in their own literature_claims table (separate from the signed claims table). FTS5 BM25 search escapes embedded quotes; the narrative exporter flags structural and polarity-heuristic contradictions inline. mareforma.db.open_db_from_db_path(): opens a graph DB from a direct file path. Honours the supplied filename instead of silently re-deriving <root>/.mareforma/graph.db. rich is now a core dependency.

Changed

  • Schema is additive on every open_db(). literature_claims, literature_claims_fts (with insert / delete / update triggers), and agent_activities tables are created via an _ADDITIVE_TABLES_SQL script that runs on both fresh and v1-initialised graphs. Existing v0.3.2 databases pick up the new tables on first open with no migration required.
  • cli.py lazy-loads ingest / ask / narrative subcommands so mareforma --help / --version / bootstrap / validator add do not pay the rich + tomli_w import cost.

Fixed

  • mareforma.derivation.source_profile: import guard catches Exception (tree-sitter ABI mismatch surfaces as TypeError / RuntimeError, not ImportError). Module-prefix matching requires a dot separator so urllib_legacy.get no longer matches the urllib import. Dead-zone walker no longer marks except clause bodies as dead (was silently demoting ANALYTICAL agents to INFERRED on error-handling paths).

Removed

  • truncate_oversized=True option on mareforma.adapters.tooluniverse.ProvenanceToolAdapter. Truncating canonicalised JSON at an arbitrary byte boundary produces bytes no replayer can re-derive; the adapter now always raises ResultTooLargeError.

v0.3.2 - 2026-05-27

Internal restructure + one restore-time verification improvement. Schema stays at v1; no migration required. All existing from mareforma.db import X and from mareforma.signing import Y import paths continue to work unchanged.

Changed

  • mareforma/signing.py split into mareforma/signing/ subpackage. signing/core.py carries DSSE PAE, canonical Statement v1, key management, envelope sign/verify, and bootstrap_key. signing/rekor.py carries Rekor submission, RFC 6962 Merkle inclusion-proof verification, checkpoint parsing, SSRF defense, and log-pubkey fetch.
  • mareforma/db.py split into mareforma/db/ subpackage. db/core.py carries the live-write path, queries, verdicts, Rekor saga, and TOML backup. db/_schema_sql.py carries the DDL constant. db/errors.py carries the exception hierarchy. db/restore.py carries restore() and its verification helpers.

Added

  • rekor_inclusions sidecar round-trip through claims.toml. _backup_claims_toml emits a [rekor_inclusions] section carrying each sidecar row’s uuid, log_index, integrated_time, raw_response_b64, and recorded_at. restore() replays entries into the sidecar table after the corresponding claim INSERT, inside the same fail-all-or-nothing transaction. Closes the restore-time gap where Merkle inclusion proofs could not be re-verified post-restore.
  • Two drift-warning classes for the sidecar restore path: RekorSidecarSectionAbsentWarning (TOML has no [rekor_inclusions] section, expected for pre-v0.3.2 files) and RekorSidecarEntryMissingWarning (section exists but lacks an entry for a Rekor-logged claim, suspicious).
  • CI guard tests walk each submodule source file via AST and assert every defined name is importable AND accessible via getattr on the package. Fails CI if a future contributor adds a name without mirroring it in __init__.py.
  • Restore-time sidecar validation. Orphan rekor_inclusions entries and entries missing required fields raise RestoreError.

Compatibility

  • claims.toml files from v0.3.0 / v0.3.1 (no [rekor_inclusions] section) restore successfully on v0.3.2 with a RekorSidecarSectionAbsentWarning. Run refresh_unsigned() after restore to re-fetch inclusion proofs from the log.

v0.3.1 - 2026-05-22

Additive release. Schema stays at v1; new columns land via in-place ALTER TABLE ADD COLUMN on the non-signed-integrity surface. First mareforma.open() after upgrade auto-adds: claims.predicate_payload, claims.original_signature_bundle, and doi_cache.content_digest. None are part of the signed envelope or chain hash, so every existing claim’s signed bytes round-trip byte-equal and signatures re-verify under the new code.

Added

  • EpistemicGraph.query_provenance(claim_id, depth=4): agent-readable lineage view of a claim: focal row + role-actor signatures + recursive upstream / downstream walks + inbound contradictions + replication verdicts in one deterministic dict.
  • Rebuildable claim_supports cache. Edge denormalisation in a separate SQLite file (.mareforma/claim_supports_cache.db). Recursive-CTE walkers serve provenance queries in O(depth × deg). Auto-rebuilt on stale / missing detection; 50k-claim p99 < 300ms.
  • claim-with-roles:v1 multi-signature DSSE envelopes. New mareforma.signing.sign_claim_with_roles + verify_envelope_multi let asserters carry per-role (planner / executor / reviewer / validator) signatures inside one envelope. Legacy single-sig envelopes verify under the existing verify_envelope unchanged.
  • PROV-O JSON-LD exporter + four-invariant hand-rolled validator. mareforma export --format=prov-o.
  • GRADE certainty surface. Optional study_design field on EvidenceVector (randomised-trial / observational / case-series / not-applicable) + new EvidenceVector.certainty() returning the GRADE four-tier band.
  • DOI metadata drift detection. New doi_cache.content_digest column + EpistemicGraph.find_drifted_dois(limit=N).
  • Refutation taxonomy + filter. New refutation_status() presenter (clean / contradicted / contested / retracted) and a composable refutation_filter kwarg on query() / search().
  • Grounding sensor protocol. New mareforma.Verifier Protocol + MockNLIVerifier reference impl. EpistemicGraph.assert_claim(grounding_sensor=verifier) snapshots the verdict (score + rationale) into the signed Statement v1 predicate at assertion time.
  • Predicate URI reservations. BUILTIN_URIS expanded from 3 to 21 entries reserving substrate-owned slots plus 18 adapter URIs.
  • Operational health log + stats CLI. Append-only .mareforma/health.jsonl records per-op operational signal. New mareforma stats [--last N] [--json] renders rolling rates.
  • Public EpistemicGraph.update_claim wrapper around db.update_claim. Status mutations are EDITORIAL: cryptographically-traceable changes use the retract-and- supersede pattern.

Changed

  • CLI: mareforma stats renamed to mareforma activity. The old stats name was one letter from the unrelated status command (snapshot vs. rolling-rate), and the homonym was a source of confusion. mareforma activity carries the same flags (--json, --last=N) and reads the same on-disk log. The mareforma stats alias still works for one release and emits a DeprecationWarning pointing at the new name; v0.4 removes the alias.
  • Wheels now ship mareforma/py.typed (PEP 561). Downstream type-checkers (mypy / pyright) now honour mareforma’s annotations instead of treating every imported symbol as Any. No source change required by callers; existing typed integration code starts seeing real errors against the substrate’s signatures.
  • mareforma.EpistemicGraph is now part of the public surface (added to __all__, importable via mareforma.EpistemicGraph). Type-hint callers no longer need to reach into the private mareforma._graph module to annotate function signatures that accept a graph handle.

Hardening

  • Mixed journal mode bug fixed, both graph.db and claim_supports_cache.db now run WAL so cross-DB transactions share atomicity guarantees.
  • Multi-sig envelopes on signature_bundle get every signature verified on restore(), not just signatures[0]. Forged extra signatures are rejected.
  • Self-validation / self-verdict gates refuse claims whose signature_bundle.signatures is empty or non-list (would otherwise let a tamperer drop their keyid from the gate).
  • find_drifted_dois aborts on the first 429 and defaults to a cap of 100 inspected DOIs per call (Crossref polite-pool guidance ~50 req/sec with two GETs per DOI).
  • Grounding sensor receives supports as an immutable tuple so a hostile or buggy verifier cannot mutate the citation list before the predicate is signed.
  • Grounding sensor exception catch widened from (VerifierError, AttributeError, TypeError, ValueError) to Exception, real-world verifiers raise OSError / ConnectionError / KeyError / RuntimeError, and the substrate’s documented “claim still lands” contract now actually holds for those.
  • health.jsonl writes use json.dumps(allow_nan=False) so a NaN score never produces non-portable JSONL that breaks jq / browser JSON.parse.
  • PROV-O exporter refuses non-UUID claim_ids (parity with RO-Crate); _extract_metadata_subset returns None on empty subsets so first-seen seeding doesn’t collapse every empty- metadata DOI to the same digest.

Compatibility

  • Legacy single-signature claim envelopes verify under verify_envelope unchanged.
  • EvidenceVector.to_dict() omits study_design / grounding_score / grounding_rationale when None so the canonical bytes of a previously-signed claim round-trip byte-equal under the new verifier.
  • Re-registering one of the 18 newly-reserved adapter URIs (anything other than claim:v1, epistemic-graph:v1, claim-with-roles:v1) by a foreign owner is downgraded from raise to DeprecationWarning for one release, adapters that pre-registered before promotion get one cycle to drop the call before the next version refuses outright. The three core substrate-owned URIs still raise hard on foreign re-registration.
  • predicate_payload is intentionally NOT part of the idempotency reconciliation surface, a retry with the same idempotency_key but divergent predicate_payload silently returns the first writer’s claim_id and discards the second payload. The field is a query-side denormalisation, not cryptographic identity; adapters that need predicate-body integrity must encode the body in the claim text instead.

v0.3.0 - 2026-05-13

Breaking change from v0.2.x. Schema does not migrate from older versions; delete .mareforma/graph.db to start fresh. claims.toml at the project root is a human-readable record of the prior state: the prev_hash chain and per-claim signatures cannot be reconstructed from it, so it is a reference not a backup. What ships in v0.3.0:
  • Ed25519 claim signing with optional Sigstore-Rekor transparency log
  • Artifact-hash gate on REPLICATED: converging peers that both supply a SHA-256 must agree
  • Identity-gated graph.validate() with a per-project validators table and signed enrollment chain
  • DOI resolution against Crossref + DataCite with a persistent cache
  • DB-layer state-machine triggers + append-only prev_hash chain: the storage layer rejects illegal transitions
  • Cycle / self-loop detection on supports[] at INSERT and UPDATE
  • ESTABLISHED-upstream requirement for REPLICATED + signed seed-claim bootstrap (Cochrane / GRADE evidence chains; no replication-of-noise)
  • JSON-LD export in a mareforma-native vocabulary
  • SCITT-style signed export bundle + mareforma verify CLI
  • In-toto Statement v1 + DSSE v1 PAE envelope on every signed claim, GRADE 5-domain EvidenceVector inside every signed predicate, signed verdict-issuer protocol that any third party can integrate against (see below)
  • RFC 8785-strict canonical JSON for every signed payload: cross-language verifiers in Go, Rust, or JS now read byte-identical bytes from a mareforma envelope. Adds rfc8785>=0.1 runtime dep.
  • Operator surfaces: graph.health() single-call audit summary, graph.refresh_convergence() to retry promotions whose detection swallowed an error, graph.refresh_all_dois() to force-re-check DOIs for retraction drift, graph.find_dangling_supports() to audit UUID refs that point nowhere, graph.classify_supports() to inspect the substrate’s claim/doi/external classification.
  • Validation envelope binds evidence_seen: pass graph.validate(claim_id, evidence_seen=[upstream_id, ...]) to record which claims the validator reviewed before signing. Bound into the signed payload alongside (claim_id, validator_keyid, validated_at). Empty list is a positive “I reviewed nothing” admission. Substrate verifies each cited claim exists and predates validation.
  • Rekor saga atomicity via a new rekor_inclusions sidecar table. When a Rekor submission succeeds but the local row-UPDATE fails, the sidecar preserves the coords so refresh_unsigned() replays the UPDATE without re-submitting (no duplicate log entries). Append-only at the trigger level.
  • Strict UUIDv4 in claim_id pattern. Non-v4 UUID-shapes in supports[] are now classified as external references rather than dangling claim_ids.
  • RFC 6962 Merkle inclusion-proof verification (opt-in). Pass rekor_log_pubkey_pem (or rekor_log_pubkey_path) to mareforma.open() and every signed-claim submit + every refresh_unsigned() re-fetches the entry from Rekor and cryptographically verifies the Merkle audit path against the log’s signed checkpoint. Verification failure refuses to mark transparency_logged=1. Supports Ed25519 (private Rekor) + ECDSA secp256r1 (public Sigstore Rekor). The supplied PEM persists to .mareforma/rekor_log_pubkey.pem as a TOFU pin, silent rotation is refused on subsequent opens; the first-pin write is atomic (O_CREAT|O_EXCL). New RekorInclusionError exception with a stable .reason token taxonomy. Since v0.3.2 the rekor_inclusions sidecar round-trips through claims.toml; restore(rekor_log_pubkey_pem=...) re-verifies each entry’s inclusion proof against the pinned key.
  • Defense-in-depth on db.validate_claim. Direct callers of the substrate function (not just EpistemicGraph.validate) get the full gate sequence: cryptographic envelope verification, LLM-type ceiling refusal, self-validation refusal, payload-field equality vs the row + kwargs. New InvalidValidationEnvelopeError for structural / cryptographic envelope failures, distinct from EvidenceCitationError for citation-list failures.
  • All documented exceptions re-exported at the top level. from mareforma import RekorInclusionError works without remembering the submodule path. 19 exception classes total, alphabetical under MareformaError.
Envelope upgrade + verdict-issuer protocol (substrate-launch additions):
  • In-toto Statement v1 + DSSE v1 PAE envelope. Every signed claim is now a DSSE envelope (payloadType=application/vnd.in-toto+json) wrapping an in-toto Statement v1 (predicateType=urn:mareforma:predicate:claim:v1). Standards-aligned; introspectable by cosign, GUAC, and any in-toto-aware tool without a mareforma-specific verifier. The signature covers the DSSE Pre-Authentication Encoding (PAE), not the payload bytes alone: a signature on (typeA, payload) cannot be replayed as a signature on (typeB, payload).
  • GRADE 5-domain EvidenceVector carried inside every signed claim’s predicate. Five downgrade domains (risk_of_bias, inconsistency, indirectness, imprecision, publication_bias) each in [-2, 0], three upgrade flags (large_effect, dose_response, opposing_confounding), rationale dict (required for any nonzero domain), and reporting_compliance list. Bound into the signature; denormalized into ev_* columns for queryable filters; restore re-derives the canonical bytes and refuses any TOML-tampered upgrade.
  • Verdict-issuer protocol. Two new tables, replication_verdicts and contradiction_verdicts, accept signed verdicts from any enrolled validator. The OSS substrate ratifies what enrolled identities sign; the predicates that PRODUCE verdicts (semantic-cluster, cross-method, hash-match, shared-resolved-upstream, contradiction-detection) live outside the OSS and call Graph.record_replication_verdict() / Graph.record_contradiction_verdict(). New VerdictIssuerError exception covers the gates: signer must be enrolled (chain walk back to a self-signed root), referenced claim must exist, method must be in the allowed enum, contradiction member != other.
  • t_invalid derived state. New nullable column on claims. The contradiction_invalidates_older AFTER INSERT trigger on contradiction_verdicts sets t_invalid on the older of the two referenced claims (lex-smaller claim_id as deterministic tie-break when timestamps collide; idempotent via WHERE t_invalid IS NULL). validate_claim refuses to promote a t_invalid claim: a signed contradiction is terminal evidence.
  • include_invalidated kwarg on graph.query(), graph.search(), graph.replication_verdicts(), graph.contradiction_verdicts(). Defaults to False: invalidated claims and the verdicts that reference them are excluded from default reads. Pass True for audit / history queries.
  • Append-only over the signed predicate. New claims_signed_fields_no_laundering BEFORE UPDATE trigger refuses direct-SQL mutation of any signed-predicate column on rows whose signature_bundle IS NOT NULL. Value-comparison fires only when something actually changed, so multi-column UPDATEs that re-emit unchanged values pass through. A tampered Python interpreter cannot relax this.
  • Append-only verdicts. *_append_only + *_no_delete triggers refuse UPDATE on signed columns and any DELETE on both verdict tables. The envelope is the source of truth.
  • PRAGMA foreign_keys = ON. Set on every open_db(). The verdict tables’ FK references to validators(keyid) and claims(claim_id) are now enforced: direct-SQL INSERTs with fabricated keyids fail at the SQL layer, not just in Python.
  • Subject ↔ predicate consistency. claim_predicate_from_envelope() refuses envelopes where subject[0].name or subject[0].digest.sha256 disagree with the predicate’s claim_id or text. Caught at the envelope-decode layer.
  • Restore extensions. claims.toml round-trip now covers both verdict tables (signatures base64-encoded). Each verdict’s signature is cryptographically verified against the enrolled issuer’s pubkey before INSERT. Verdicts are replayed in created_at order so the trigger’s WHERE t_invalid IS NULL guard preserves the truthful first-invalidation moment. transparency_logged=true in TOML is downgraded to 0 when the bundle has no rekor block: hand-edited TOML cannot fake a Rekor inclusion. New adversarial tests for tampered EvidenceVector, swapped statement_cid, tampered verdict fields, and forged issuer_keyid.
  • New modules: mareforma._canonical (NFC + sorted-keys + no-whitespace + allow_nan=False canonical JSON), mareforma._statement (in-toto Statement v1 builder + statement_cid computation), mareforma._evidence (stdlib-dataclass EvidenceVector with __post_init__ validator). No pydantic dependency added; mareforma stays at 5 runtime deps.
  • mareforma.signing.dsse_pae() is public so external verifiers can independently re-derive the bytes the signature covers. canonical_statement(claim_fields, evidence) replaces the legacy canonical_payload for chain-hash + signature inputs; the old shim is removed because it silently desynced from production bytes.

Identity, signing, transparency

  • Ed25519 claim signing. mareforma bootstrap once to generate a keypair at ~/.config/mareforma/key (XDG-compliant, mode 0600). Every assert_claim then signs before INSERT. The signed payload binds claim_id, text, classification, generated_by, supports, contradicts, source_name, artifact_hash, and created_at: any tamper breaks verification.
  • Append-only invariant. Signed claims refuse mutation of any signed-surface field. update_claim(text=...) / update_claim(supports=...) / update_claim(contradicts=...) on a signed row raise SignedClaimImmutableError. status and comparison_summary remain editable.
  • Sigstore-Rekor transparency log. mareforma.open(rekor_url=mareforma.signing.PUBLIC_REKOR_URL) submits every signed claim at INSERT time. Submission failure persists the claim with transparency_logged=0 and blocks REPLICATED until graph.refresh_unsigned() succeeds.
  • Identity-gated graph.validate(). The loaded signer 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). The validation event itself is signed: a DSSE-style envelope binding (claim_id, validator_keyid, validated_at) is persisted to the row’s validation_signature column.
  • New mareforma validator add / mareforma validator list subcommands. Each enrollment is signed by the parent validator and is_enrolled walks the chain back to a self-signed root before accepting a row: direct sqlite INSERTs with a fabricated parent do not pass. Singleton-root invariant + 64-hop walk cap defend against DoS-by-planted-chain.

Storage-layer state machine

  • DB-layer state-machine triggers. Two BEFORE triggers enforce PRELIMINARY → REPLICATED → ESTABLISHED at the storage layer; direct PRELIMINARY → ESTABLISHED is rejected; ESTABLISHED rows require validation_signature. Illegal transitions surface as IllegalStateTransitionError with a parsed <from>-><to> string instead of an opaque CHECK CONSTRAINT FAILED.
  • Append-only hash chain. New claims.prev_hash column carries sha256(prev_chain_link || canonical_payload). UNIQUE partial index + BEGIN IMMEDIATE together prevent branched chains from concurrent writers or manual SQL tamper. New ChainIntegrityError.
  • Cycle / self-loop detection. A claim whose supports[] would create a cycle (directly or via a chain) raises CycleDetectedError at INSERT and at UPDATE. Forward-walk DFS, depth-capped at 1024 hops. DOI strings in supports[] are not graph nodes and skipped.
  • ESTABLISHED-upstream requirement for REPLICATED. REPLICATED promotion now requires at least one ESTABLISHED claim in the peer’s supports[]. Matches Cochrane / GRADE evidence-chain methodology: stops replication-of-noise. Strict by default.
  • Seed-claim bootstrap. graph.assert_claim(text=..., seed=True) inserts a claim directly at ESTABLISHED with a signed seed envelope (payload type application/vnd.mareforma.seed+json, binds claim_id + validator_keyid + seeded_at). Only enrolled validators can produce seeds: bootstraps the trust chain on a fresh graph without a back door.

Artifact-hash gate

  • artifact_hash parameter on assert_claim (Python API) and --artifact-hash flag on mareforma claim add (CLI). Accepts a SHA-256 hex digest of the output bytes (figure, CSV, model) backing the claim. Normalised to lowercase, validated as 64-char hex, persisted to the new artifact_hash column, and bound into the signed payload.
  • REPLICATED gate. When two converging peers BOTH supply a hash, the hashes must match for REPLICATED to fire. When either omits the hash, the gate is bypassed and identity-only REPLICATED applies: the signal is opt-in, not retroactive.
  • Idempotency conflict. A replay that supplies a different artifact_hash than the original raises IdempotencyConflictError rather than silently dropping the new hash.

Prompt-safety substrate

  • mareforma.prompt_safety module + graph.query_for_llm(). Sanitize-and-wrap helpers for feeding retrieved claim text into an LLM prompt. Strips zero-width / bidi-override / C0-C1 control characters, Goodside U+E0000 tag plane, variation selectors, interlinear annotation anchors, and the fullwidth </>// lookalikes. Caps oversized fields at 100k chars with a visible truncation marker. Free-text fields are wrapped in <untrusted_data>...</untrusted_data>; forged delimiter tags inside the content are replaced with [stripped].
  • get_tools() routes through query_for_llm. The query_graph tool that ships to LangChain / LangGraph / CrewAI / AutoGen / LlamaIndex / PydanticAI / Smol Agents / OpenAI SDK / Anthropic SDK now returns sanitized + wrapped text. A stored prompt-injection planted by a prior agent is no longer delivered verbatim to the consuming LLM.
  • Sanitize-on-write. assert_claim runs sanitize_for_llm(text) before signing and persisting. Defense in depth: any consumer that reads claim.text directly gets a clean string. Hard cap of 100,000 characters; claims that consist entirely of zero-width / control characters are rejected with ValueError.

Export

  • JSON-LD export: mareforma-native vocabulary. Removed PROV-O references (prov:wasGeneratedBy, prov:used) from the JSON-LD @context: the previous export name-dropped the vocabulary without populating the full PROV-O graph. The export now declares @type='mare:Graph' and mare:mediaType='application/x-mareforma-graph+json'. The used key on source-bearing claims was renamed to usedSource (aliased to mare:usedSource). Every SIGNED_FIELDS member is always emitted on each claim node so downstream consumers (e.g. the bundle verifier below) can re-derive canonical_payload from a node alone.
  • SCITT-style signed bundle. New mareforma export --bundle produces an in-toto Statement v1 wrapper around the JSON-LD export, with predicateType='urn:mareforma:predicate:epistemic-graph:v1' and a DSSE-style signature over the whole bundle. Subject names use the urn:mareforma:claim:<uuid> namespace; URN (not DNS) avoids a perpetual-ownership commitment on mareforma.dev. New mareforma verify <bundle.json> checks the DSSE signature AND every per-claim subject digest. New BundleVerificationError names the first failing check so callers can route between “corrupt” and “cross-version skew”.

DOI verification

  • DOI resolution: every DOI in supports[]/contradicts[] is HEAD-checked against Crossref and DataCite at assert time. Unresolved DOIs mark the claim unresolved=True and block REPLICATED promotion. EpistemicGraph.refresh_unresolved() retries previously-failed resolutions.
  • DOI resolver hardening: DOI suffix URL-encoded before interpolation (prevents host injection via #/@); follow_redirects=False (registry must answer directly); pooled httpx.Client with threading lock around lazy init (FD-leak-safe under concurrency); HTTP 429 from either registry skips the cache write; tight exception clause so programmer bugs surface in tracebacks.
  • doi_cache table: 30-day TTL for resolved entries, 24-hour TTL for unresolved.

Supply chain

  • PyPI Trusted Publishing. Releases are published via OIDC-based GitHub Actions, not long-lived API tokens. pypa/gh-action-pypi-publish is SHA-pinned. actions/checkout and actions/setup-python are pinned by commit SHA: closes the tag-squat / maintainer-compromise vector against the Trusted Publishing OIDC token.
  • New SECURITY.md documents the disclosure channel (GitHub Private Vulnerability Reporting), supported-versions policy (latest pre-1.0 only), PyPI Trusted Publishing setup, cryptographic trust boundaries, and out-of-scope categories.
  • Typosquat reservations. maraforma, mareform, mareforma-cli, mareforma-py, and mareforma-agent are reserved on PyPI as defensive placeholders that raise ImportError and point users back to the canonical package. mare-forma / mare_forma / mare.forma are auto-blocked by PyPI’s confusable-name check.
  • New .github/CODEOWNERS and .github/dependabot.yml.

Agent surface

  • mareforma.open() returns an EpistemicGraph: no @transform required. New parameters: key_path, require_signed, rekor_url, require_rekor, trust_insecure_rekor.
  • EpistemicGraph methods: assert_claim, query, search, query_for_llm, get_claim, validate, refresh_unresolved, refresh_unsigned, enroll_validator, list_validators, get_validator_reputation, get_tools, close.
  • get_tools(generated_by="agent/...") returns [query_graph, assert_finding] as plain Python callables. One-line wrap for Anthropic SDK, OpenAI SDK, LangChain, LangGraph, CrewAI, AutoGen, LlamaIndex, PydanticAI, Smol Agents.
  • mareforma.schema(): runtime introspection of valid values, defaults, state transitions, and schema version.
  • mareforma.restore(project_root): rebuild a fresh graph.db from claims.toml for catastrophic-loss recovery. Fresh-only, fail-all-or-nothing on signature verification.
  • CLI: mareforma bootstrap, mareforma validator add / validator list (with --type human|llm), mareforma claim add/list/show/update/validate, mareforma status, mareforma export [--bundle], mareforma verify <bundle>, mareforma restore [<toml-path>].

Validator type and reputation

  • Validator type signal. validators.validator_type TEXT CHECK IN ('human','llm'), bound into the signed enrollment envelope. Default 'human'. The substrate refuses promotion past REPLICATED on an LLM-typed validator’s signature alone (LLMValidatorPromotionError); a human-typed co-signer is required. Self-validation (claim signer == validation signer) is also refused (SelfValidationError).
  • Reputation-aware retrieval. query() and search() gain include_unverified: bool = False. PRELIMINARY claims whose signing key is not in the validators table are excluded by default. Result dicts carry derived validator_reputation (count of ESTABLISHED claims signed by the same validator) and generator_enrolled (bool). graph.get_validator_reputation() returns the bulk {keyid: count} map.
  • FTS5 over claim text. New claims_fts virtual table (unicode61 tokenizer, diacritics folded) synced with claims via three INSERT/DELETE/UPDATE-of-text triggers. New graph.search() method exposes FTS5 ranked match. Phrase, prefix, boolean, and proximity operators all supported. Pure-wildcard queries refused.

claims.toml round-trip + restore

  • claims.toml format extended. A [validators] section now travels alongside [claims], carrying signed enrollment envelopes so the restore path can verify the chain. Old files with no [validators] section continue to work as unsigned-mode.
  • mareforma restore (CLI + Python API). Fresh-only rebuild from claims.toml. Refuses non-empty graph.db. Verifies every signature before any row is inserted. New RestoreError with .kind field naming the failure mode (graph_not_empty, toml_not_found, toml_malformed, enrollment_unverified, claim_unverified, mode_inconsistent, orphan_signer). Adversarial test class proves the round-trip catches tampered text, mutated signature bytes, missing signatures in signed-mode graphs, orphan signers, and validator-row tampering.
  • _backup_claims_toml failure to stderr at ERROR-level (was warnings.warn, which production loggers routinely suppress). graph.db remains authoritative.

Removed

  • @transform decorator and BuildContext: pipeline layer removed.
  • MareformaObserver, LangChainAdapter: execution tracing removed.
  • Pipeline CLI commands: init, add-source, explain, build, log, diff, cross-diff, trace.

v0.2.1 - 2026-05-08

  • ctx.params: runtime parameter injection from TOML
  • query_claims(): read primitive for the epistemic graph
  • delete_claims_by_generated_by(): delete claims by source agent
  • Fixed LangChainAdapter import path

v0.2.0 - 2026-04-08

  • mareforma.agent: framework-agnostic agent provenance module
  • MareformaObserver: context manager recording agent events to graph.db
  • LangChainAdapter: LangChain callback handler

v0.1.0 - 2026-03-25

Initial release. @transform decorator, ctx.claim(), mareforma build, SQLite epistemic graph, claims.toml backup.