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 readNOT_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 indropped_nullsrather than run. - The profile rule.
INFLUENCEDneeds every null to move the finding,NOT_INFLUENCEDneeds none of them to, and anything between isUNDECIDABLE. 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 readNOT_TESTEDnaming which. The three measurement numbers areNoneon 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 auditruns its target once and never perturbs it, so every edge on its receipts readsNOT_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_excludedandverify_excludedon the MCP pages, andread_unverified_exclusionson 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.selfcheckships in the wheel: four seeded failures a correct instrument must catch, runnable without the source tree.
Changed
diagnoseandauditexit 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.v2andmodel.v1.2run;runspec.json.1andapp.log.1stay refused. claim listtakes--limitand says when it capped. The flag is opt-in, so the JSON output is unchanged for anything already parsing it.claim showmarks an unbacked support level UNVERIFIED, asclaim listalready did. It is the command an auditor runs on the claim they suspect.
Fixed
- A withheld row no longer truncates the record.
has_morewas 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_claimcalled it tampered. It is withheld and counted now. verify_claimno 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 as10−9and6×10²³as6×1023while 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
completedwithout 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.UsageErrorraised 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 thatverify 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 serveserves one project over the Model Context Protocol behind the optionalmcpextra. 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_statusreportsquestion_status.consistentordividednames the state of the question, besidestatusfor 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/07runs two pipelines that print the same number, and shows only the one that read its data reading GROUNDED.
Changed
-
diagnoserefuses 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 drawUNGROUNDEDwithscope fully observedbeside it: a false accusation carrying a false completeness claim. It is now a usage error, exit 2, before the target runs.auditalready 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_fromfield. 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 serverefuses 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 namedgraph.dbor sitting inside a.mareforma/directory that is not already a project. Both raiseValueError. Pointing the library at the database file rather than the project directory is the mistake this catches. -
mareforma statuscan 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. -
JSONLDExporterrequiresclassificationandsupport_levelon every row it is handed. It used to default them silently. First-party callers pass fulllist_claimsrows and are unaffected; a caller passing a projected row toexport(claims=...)now gets an error instead of a fabricated node. -
mareforma mcp serverefuses 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
-
verifyno longer reports a claim verified when nothing was checked. A signature bundle whosesignaturesarray 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. -
verifyno longer reports an unsigned claim as verified. A claim with no signature reached none of the signature checks, so the verdict fell through toverifiedand exit 0. It is nowunverifiable, exit 2. This was reachable with no database access and no attacker: open a project, assert a claim, never runbootstrap. That is the path the quickstart teaches and the pathexamples/06_ci_verifygates 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 asunsigned; the verdict never read its own map. -
The witnessing axis reports the record it found, not a proof it did not
check. It read
loggedwith the residual “signed and recorded in a transparency log with an inclusion proof” whenever a row existed inrekor_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 readsinclusion record presentand 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_invalidalone. 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 nowinvalidation-recordedrather thansigned-verdict. -
verifyno 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
verifycalled 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 throughmareforma mapandgraph.trust_map();verifywas correct throughout. -
Short label fields can no longer forge a closing delimiter.
source_name,generated_byandvalidated_byskip 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 inquery_for_llm.
Deprecated
- The support ladder, whole. 0.4.0 removes the
PRELIMINARY/REPLICATED/ESTABLISHEDlabels, the storedsupport_levelcolumn, the promotion machinery, and thequery(min_support=...)filter. Read the trust map’s independence axis, orproposition_status(), for how much distinct backing a finding has. Both surfaces warn today: the retired module labels on attribute access, andmin_supporton 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_statusonproposition_status. It echoed the answer’s own status word. Readquestion_statusinstead. 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()’squery_graphno longer defaultsmin_supportto"PRELIMINARY". That value is the floor of the ladder, so it filtered nothing, but it reachedqueryas 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
verifyandtrust_mapfixes 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: twoopen()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=Truerecords 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.dbrecords 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
chemextra 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_hocflag onproposition_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
TrustMapVersionErrorinstead 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 forgedmodel_lineagecolumn can inflate toCONVERGENT. 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_byis 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 readsUNTESTED,lines_skippedrises, and each drop is namedunregistered_signer_skippedin.mareforma/health.jsonl. There is no un-enrol. Open a project with its key from the first write, or keep it unsigned. -
restoreaccepts 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 carriesunsigned_in_signed_modecounting them, and a warning names the first few. They still count on no axis, so the read path andrestorenow describe the same graph. A claim carrying astatement_cidwith nosignature_bundleis 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_retirementsrow stops resolving stranded lines through it. A row whose claim does not render the plan, replacement and reason it names resolves nothing, andrestorerefuses 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 statusnow names the stuck count asconvergence_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.
-
Predictionno longer acceptspreregistered. The constructor keyword is gone andto_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. -
Predictionrefuses 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 raisesValueErrorat 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 withretire_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_receiptsraisesGroundingAxisMismatchErroron any receipt from v0.3.9 or v0.3.10 andmareforma measureexits 1 on a storedreceipts.jsonl. Re-run the observation to produce receipts on the current axis. -
perturbation_oraclereturns 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 verifyreports a foreign-key bundle as unverifiable, not tampered. A bundle signed with a key other than the local one now exits 2 with verdictunverifiable, where it exited 1 withtampered. A CI gate keyed on the documented 1-versus-2 split takes the opposite branch. Pass--keyto verify against the signing key. -
mareforma reexecusage 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 addwrites to the nearest ancestor project.claim add,claim update,claim validateandvalidator addnow 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_contradictedcounts 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
UnverifiedClaimErrornaming the claims when any row fails verification, andmareforma exportexits 1. They also raiseFileNotFoundErroron a root with no graph instead of returning an empty document, as doesexport_bundle.build_statement. -
The RO-Crate
signatureproperty 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 callingjson.loadson 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
%3Awhere it rendered as_, so documents exported by different releases do not join on the agent node. -
mareforma export --bundle --jsonexits 1. The combination was a documented no-op and is now refused as mutually exclusive, so a script passing a blanket--jsonto every export fails. -
write_bundlerefuses 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 withBundleExportError. Projects whose earliest claims predate their signing key are the common case. -
register_canonicalizerrefuses a duplicate name. Re-registering an existing form now raises; passoverride=Trueto 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 mapsevidenceas a JSON literal, so it survives expansion where it was previously dropped. -
mareforma validator listcan exit 1. A read-only listing now fails when the validator rows do not chain back to a single self-signed root, which aborts aset -escript that only meant to list them. -
mareforma audit --outreplaces 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. -
partialis true for any non-zero exit. Indiagnose --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_doino 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_supportsreturns a different answer for unchanged stored data. -
Declaring the strict-promotion policy requires the project’s root key. Opening
with
strict_promotion=Truefrom a keyless or non-root caller raisesProjectPolicyError, where the flag was previously accepted on any handle. This is the enforcement half of the policy entry under Added.
Changed
verify_claim_signaturesreturnsFalsefor 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 statuscannot 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,propositionsandvalidatorsare append-only and undeletable, andpredictions.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 guardfindingsandevidence_linesalready carried.claims.tomlkeeps a.prevcopy 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_claimsaccepts alimit, so a caller can bound the verify-on-read work on a large graph the wayqueryandsearchalready do.effective_independencereportslines_skipped, so the independence axis does not read a confident number off a line set that silently lost lines.- Omitting
generated_byno 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 raisesPostHocPlanErroron apreregistered=1plan 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 landsUNGROUNDEDwith 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 toOPAQUE. One open more than the observed failures still landsOPAQUEon the hidden-reader gap. - A support level above
PRELIMINARYis 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 readsverified=Falseand 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_VERSIONis stamped for the new shape. compute_healthno longer takes the project root it never used.verify_rekor_inclusiontakes the signed claim envelope as a third argument, so a call written against 0.3.10 raisesTypeErroruntil 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
richdependency was never imported and is dropped. - All four empty extras are gone:
clawinstitute,tooluniverse,geminianddocs. 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
restorereports 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 raisedProgrammingError, orOverflowError, straight out ofrestore. Every trust table now rejects a non-scalar withRestoreError(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.
findingsandevidence_linesare 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_planrefuses 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.tomlcannot 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:
verifyandmapno 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 andmareforma claim listno 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_pemis 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_inclusionrefuses 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 andassert_claimsignatures, six undocumented graph methods, the refusalsvalidate()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. Runmareforma 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), orUNVERIFIABLE(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
COMPUTEDvia aweights-digestattestor: 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 athttpxsendand theaiohttpseam (the provider SDK and litellm paths), and a citedpolarsread grounds. A grounded read and a computed lineage are gated on a successful response, so an error body never grounds a cited URL, and aduckdbquery floors toOPAQUErather than a falseUNGROUNDED. 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
REPLICATEDandESTABLISHEDlabels. 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-onlyis_doihelper stays), and activity hooks. Existing graphs open unchanged.
Changed
- The top status label
CORROBORATEDis renamedCONVERGENT. 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.CONVERGENTstates 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. ReadingStatus("CORROBORATED")orStatus.CORROBORATEDstill resolves toStatus.CONVERGENTthis release and emits aDeprecationWarning; 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 fromstatus_policy@v3tostatus_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
digestis 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_provenancestops serving pre-edit or dangling lineage. - Graph mutations serialize across threads,
update_claimapplies the same write invariants asadd_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 verifyreads a signed auditrun.jsonas unverifiable (trusted via resume), not as tampered.- CLI:
exportandactivitydiscover 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
auditandreexec, and a drift guard fails when any shipped command leaves the reference.TRUST_MAP_VERSIONmoves tov0.3.10.
Performance
- The findings table carries a
claim_idindex 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
setuptoolsbuild floor is raised for the string license metadata, and thetest-heavyextra 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 agrounding_citation_mismatchhealth event, or raises undergrounding_strict=True. Evidence lines gaindata_source=so citing a path and content-addressing thedata_idbinds. - The per-finding trust map.
graph.trust_map(claim)andmareforma 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.--jsonemits a canonicalizable record;--htmlwrites 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 (0verified,1tamper or binding violation,2unverifiable,3usage error), so a bad flag never masquerades as a verdict.--jsonemits 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--citesit 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 --doctorandmareforma measure.--doctorself-reports which loaders are wrapped and which seams force OPAQUE in the current environment.measureaggregates a run’s verdicts into the reported split, OPAQUE bucketed by seam kind.--redact-homerewrites$HOMEin emitted artifacts, never in a signed receipt.- Keep-alive HTTP and C-extension coverage. Pooled
requests.Session,httpx.Client/AsyncClient, andaiohttpsessions, 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
UNVERIFIABLEwhenever fewer than two trust roots are enrolled, the honest reading when one operator could mint every key. strict_promotionoption.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 wasassert_finding, which shadowed theEpistemicGraph.assert_findingmethod. A deprecatedassert_findingalias is available for one release viaget_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
GraphTooLargeErrorrather 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
datePublishedand separates data entities (hasPart) from provenance actions (mentions); PROV-O labels userdfs:label. The tests check shape and label vocabulary, not full validator conformance. mareforma verifysubsumes the old bundle-path command. The priormareforma verify <bundle>invocation keeps working as the file-detection case; a missing local key exits2(unverifiable) rather than1.- 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
SignedClaimImmutableErrorfor bothdelete_claimanddelete_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_countsstops full-scanningeffect_estimates, via an index oncontrasts(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 --formatchoices, 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 thetree_sitterdependencies are gone; execution-observed grounding computes the same signal from observed reads. - The dead
[git]install extra and the unusedgitpythondev 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_bundlechecks 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, matchingquery().- Convergence respects signed contradiction verdicts; a stranded re-check is retried.
claims.tomlis written atomically and can be batched withgraph.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 --helpnow documents the requiredTITLE/DOI/CLAIMSlayout, 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, andquery_provenancere-verify aREPLICATEDorESTABLISHEDrow’s signatures before serving it.query_*excludes a row whose signature does not verify;get_claimflags itverified=False. Neither raises. The check binds the signed payload to the row. single_trust_domaindisclosure on eachESTABLISHEDrow in query results and the exported bundle: whether every validator traces to one root of trust. A disclosure, not a Sybil guard.asserter_keyidcolumn, denormalized from the signature bundle and read by both the promotion query and the trust-layer count. Added to existing graphs byALTER TABLE ADD COLUMNon first open.
Changed
REPLICATEDkeys on the signing key, notgenerated_by. Two claims sharing anESTABLISHEDupstream converge only when distinct keys sign them.generated_bybecomes a display label. ExistingREPLICATEDrows are grandfathered with alegacy_promotionhealth event. A convergence signal, not proof: distinct keys do not prove the data is independent.Statusindependence keys on the signing key (the same axis as promotion), with adata_idguard; unsigned lines fall back to the retired distinct-generated_byrun axis. Policy moves fromstatus_policy@v2tostatus_policy@v3, recomputed on read.artifact_hashis 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_idis 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, andStatus 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_findingtake alines=[EvidenceLine, ...]argument in place of the singleestimate+data_idpair, recording several datasets or arms under one proposition and prediction. A finding with no lines raisesValueError; a finding where any line fails the gate rolls back whole. The finding’s identity is its fulldata_idset. The return dict gainsbearings, 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
Statusindependence is now run-distinct: support and refute count distinctgenerated_by(run) with adata_idguard. One run contributes at most one support and one refute, so a single run cannot reachCORROBORATEDon its own. Two findings on one proposition from the same run that previously readCORROBORATEDnow readPRELIMINARY; findings from distinct runs are unaffected. Policy stamp moves tostatus_policy@v2, recomputed on read with no migration.- A blank or whitespace
generated_byis 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 singleassert_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 thepredictionsrow (preregistered=1) and its own signed plan attestation claim under idempotency keyplan:{plan_id}, Rekor-anchorable like any other claim. Idempotent. Returns the content-addressedplan_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 whosesupports[]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 differentplan_idraisesFindingPlanForkError.mareforma.trusterrorsNoRegisteredPlanErrorandFindingPlanForkError.mareforma.trustgates chain:Gate,gates_for(prediction), andevaluate_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 tocompute_bearing. Pure Python, no new schema column.
Changed
assert_findingnow composesregister_plan+submit_findinginternally. Its synthesised plan is flaggedpreregistered=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_planandsubmit_findingemit 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. Theabs(ci_level - expected_level) > 1e-9check 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_idis the answer (subject, relation, object, scope, direction, magnitude);frame_idis 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 asstatus_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 isp < 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 undermareforma.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 primitivesmareforma.events:EventSource/EventHandlerProtocols, typedEventPayloadandClaimResult, source-name constants (SOURCE_CLAWINSTITUTE,SOURCE_TOOLUNIVERSE,SOURCE_GEMINI,SOURCE_CLAUDE_CODE_PRETOOLUSE) so adapters dispatch on constants, not string literals.mareforma.tools:ToolProtocol (name,version,call(**kwargs) -> ToolResult),ToolResultTypedDict,ReplayResultdataclass. The structural contract any wrappable callable satisfies.mareforma.canonicalize: registry-based canonicalizer surface for adapter authors. Defaultjson-c14n-v1(RFC 8785 JCS) plusdsse-jcs-nfc-v1(same bytes the signed-envelope layer produces). Importingmareforma.canonicalizeregistersrdkit-canonical-smiles-v1,fasta-nfc-v1,pdb-atom-sorted-v1via the specialty submodule.mareforma.derivation: substrate-derived classification. Deterministically derivesANALYTICALvsINFERREDfrom 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 CodePreToolUsehandler (python -m mareforma.hooks) records every tool invocation as aprov:Activityrow.agent_activitiestable is part of the canonical schema.
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.EventHookimplements the EventSource Protocol;HttpxClientuses a pooledhttpx.Clientwithfollow_redirects=Falseand URL-quotes path segments. Eight typed exceptions shareClawInstituteApiErroras parent. Untrusted workshop content runs through three sanitisation layers (raw-byte cap →sanitize_for_llm→wrap_untrusted). Handler exceptions duringdispatch()are caught and returned asClaimResult(error=…)so a misbehaving subscriber cannot block peers.mareforma.adapters.tooluniverse: wrap anymareforma.tools.Toolso each.call(**kwargs)records a signedtool-call:v1claim with arguments digest, result digest, tool config fingerprint, timing. Container-exec class tools route tocontainer-exec:v1. Over-cap results raiseResultTooLargeError.mareforma.adapters.gemini: read-only ingest for Gemini for Science outputs (4 capabilities: code-variation, hypothesis, literature-insight, science-skill). Per-capabilityREQUIRED_FIELDSvalidation runs beforeassert_claim; string payload values flow throughsanitize_for_llm; reserved keys (predicate_type,capability) are adapter-owned.
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), andagent_activitiestables are created via an_ADDITIVE_TABLES_SQLscript 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.pylazy-loads ingest / ask / narrative subcommands somareforma --help/--version/bootstrap/validator adddo not pay therich+tomli_wimport cost.
Fixed
mareforma.derivation.source_profile: import guard catchesException(tree-sitter ABI mismatch surfaces asTypeError/RuntimeError, notImportError). Module-prefix matching requires a dot separator sourllib_legacy.getno longer matches theurllibimport. Dead-zone walker no longer marksexceptclause bodies as dead (was silently demoting ANALYTICAL agents to INFERRED on error-handling paths).
Removed
truncate_oversized=Trueoption onmareforma.adapters.tooluniverse.ProvenanceToolAdapter. Truncating canonicalised JSON at an arbitrary byte boundary produces bytes no replayer can re-derive; the adapter now always raisesResultTooLargeError.
v0.3.2 - 2026-05-27
Internal restructure + one restore-time verification improvement. Schema stays at v1; no migration required. All existingfrom mareforma.db import X and from mareforma.signing import Y
import paths continue to work unchanged.
Changed
mareforma/signing.pysplit intomareforma/signing/subpackage.signing/core.pycarries DSSE PAE, canonical Statement v1, key management, envelope sign/verify, andbootstrap_key.signing/rekor.pycarries Rekor submission, RFC 6962 Merkle inclusion-proof verification, checkpoint parsing, SSRF defense, and log-pubkey fetch.mareforma/db.pysplit intomareforma/db/subpackage.db/core.pycarries the live-write path, queries, verdicts, Rekor saga, and TOML backup.db/_schema_sql.pycarries the DDL constant.db/errors.pycarries the exception hierarchy.db/restore.pycarriesrestore()and its verification helpers.
Added
rekor_inclusionssidecar round-trip throughclaims.toml._backup_claims_tomlemits 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) andRekorSidecarEntryMissingWarning(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
getattron the package. Fails CI if a future contributor adds a name without mirroring it in__init__.py. - Restore-time sidecar validation. Orphan
rekor_inclusionsentries and entries missing required fields raiseRestoreError.
Compatibility
claims.tomlfiles from v0.3.0 / v0.3.1 (no[rekor_inclusions]section) restore successfully on v0.3.2 with aRekorSidecarSectionAbsentWarning. Runrefresh_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-placeALTER 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_supportscache. 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:v1multi-signature DSSE envelopes. Newmareforma.signing.sign_claim_with_roles+verify_envelope_multilet asserters carry per-role (planner / executor / reviewer / validator) signatures inside one envelope. Legacy single-sig envelopes verify under the existingverify_envelopeunchanged.- PROV-O JSON-LD exporter + four-invariant hand-rolled
validator.
mareforma export --format=prov-o. - GRADE certainty surface. Optional
study_designfield onEvidenceVector(randomised-trial/observational/case-series/not-applicable) + newEvidenceVector.certainty()returning the GRADE four-tier band. - DOI metadata drift detection. New
doi_cache.content_digestcolumn +EpistemicGraph.find_drifted_dois(limit=N). - Refutation taxonomy + filter. New
refutation_status()presenter (clean / contradicted / contested / retracted) and a composablerefutation_filterkwarg onquery()/search(). - Grounding sensor protocol. New
mareforma.VerifierProtocol +MockNLIVerifierreference 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_URISexpanded from 3 to 21 entries reserving substrate-owned slots plus 18 adapter URIs. - Operational health log + stats CLI. Append-only
.mareforma/health.jsonlrecords per-op operational signal. Newmareforma stats [--last N] [--json]renders rolling rates. - Public
EpistemicGraph.update_claimwrapper arounddb.update_claim. Status mutations are EDITORIAL: cryptographically-traceable changes use the retract-and- supersede pattern.
Changed
- CLI:
mareforma statsrenamed tomareforma activity. The oldstatsname was one letter from the unrelatedstatuscommand (snapshot vs. rolling-rate), and the homonym was a source of confusion.mareforma activitycarries the same flags (--json,--last=N) and reads the same on-disk log. Themareforma statsalias still works for one release and emits aDeprecationWarningpointing 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 asAny. No source change required by callers; existing typed integration code starts seeing real errors against the substrate’s signatures. mareforma.EpistemicGraphis now part of the public surface (added to__all__, importable viamareforma.EpistemicGraph). Type-hint callers no longer need to reach into the privatemareforma._graphmodule to annotate function signatures that accept a graph handle.
Hardening
- Mixed journal mode bug fixed, both
graph.dbandclaim_supports_cache.dbnow run WAL so cross-DB transactions share atomicity guarantees. - Multi-sig envelopes on
signature_bundleget every signature verified onrestore(), not just signatures[0]. Forged extra signatures are rejected. - Self-validation / self-verdict gates refuse claims whose
signature_bundle.signaturesis empty or non-list (would otherwise let a tamperer drop their keyid from the gate). find_drifted_doisaborts 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
supportsas 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)toException, real-world verifiers raise OSError / ConnectionError / KeyError / RuntimeError, and the substrate’s documented “claim still lands” contract now actually holds for those. health.jsonlwrites usejson.dumps(allow_nan=False)so a NaN score never produces non-portable JSONL that breaksjq/ browserJSON.parse.- PROV-O exporter refuses non-UUID claim_ids (parity with
RO-Crate);
_extract_metadata_subsetreturns 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_envelopeunchanged. EvidenceVector.to_dict()omitsstudy_design/grounding_score/grounding_rationalewhen 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_payloadis intentionally NOT part of the idempotency reconciliation surface, a retry with the sameidempotency_keybut divergentpredicate_payloadsilently returns the first writer’sclaim_idand 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_hashchain: 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 verifyCLI - In-toto Statement v1 + DSSE v1 PAE envelope on every signed claim, GRADE 5-domain
EvidenceVectorinside 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.1runtime 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: passgraph.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_inclusionssidecar table. When a Rekor submission succeeds but the local row-UPDATE fails, the sidecar preserves the coords sorefresh_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(orrekor_log_pubkey_path) tomareforma.open()and every signed-claim submit + everyrefresh_unsigned()re-fetches the entry from Rekor and cryptographically verifies the Merkle audit path against the log’s signed checkpoint. Verification failure refuses to marktransparency_logged=1. Supports Ed25519 (private Rekor) + ECDSA secp256r1 (public Sigstore Rekor). The supplied PEM persists to.mareforma/rekor_log_pubkey.pemas a TOFU pin, silent rotation is refused on subsequent opens; the first-pin write is atomic (O_CREAT|O_EXCL). NewRekorInclusionErrorexception with a stable.reasontoken taxonomy. Since v0.3.2 therekor_inclusionssidecar round-trips throughclaims.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 justEpistemicGraph.validate) get the full gate sequence: cryptographic envelope verification, LLM-type ceiling refusal, self-validation refusal, payload-field equality vs the row + kwargs. NewInvalidValidationEnvelopeErrorfor structural / cryptographic envelope failures, distinct fromEvidenceCitationErrorfor citation-list failures. - All documented exceptions re-exported at the top level.
from mareforma import RekorInclusionErrorworks without remembering the submodule path. 19 exception classes total, alphabetical underMareformaError.
- 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 bycosign, 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),rationaledict (required for any nonzero domain), andreporting_compliancelist. Bound into the signature; denormalized intoev_*columns for queryable filters; restore re-derives the canonical bytes and refuses any TOML-tampered upgrade. - Verdict-issuer protocol. Two new tables,
replication_verdictsandcontradiction_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 callGraph.record_replication_verdict()/Graph.record_contradiction_verdict(). NewVerdictIssuerErrorexception 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, contradictionmember != other. t_invalidderived state. New nullable column onclaims. Thecontradiction_invalidates_olderAFTER INSERT trigger oncontradiction_verdictssetst_invalidon the older of the two referenced claims (lex-smallerclaim_idas deterministic tie-break when timestamps collide; idempotent viaWHERE t_invalid IS NULL).validate_claimrefuses to promote at_invalidclaim: a signed contradiction is terminal evidence.include_invalidatedkwarg ongraph.query(),graph.search(),graph.replication_verdicts(),graph.contradiction_verdicts(). Defaults toFalse: invalidated claims and the verdicts that reference them are excluded from default reads. PassTruefor audit / history queries.- Append-only over the signed predicate. New
claims_signed_fields_no_launderingBEFORE UPDATE trigger refuses direct-SQL mutation of any signed-predicate column on rows whosesignature_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_deletetriggers 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 tovalidators(keyid)andclaims(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 wheresubject[0].nameorsubject[0].digest.sha256disagree with the predicate’sclaim_idortext. Caught at the envelope-decode layer. - Restore extensions.
claims.tomlround-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 increated_atorder so the trigger’sWHERE t_invalid IS NULLguard preserves the truthful first-invalidation moment.transparency_logged=truein TOML is downgraded to0when the bundle has norekorblock: hand-edited TOML cannot fake a Rekor inclusion. New adversarial tests for tamperedEvidenceVector, swappedstatement_cid, tampered verdict fields, and forgedissuer_keyid. - New modules:
mareforma._canonical(NFC + sorted-keys + no-whitespace +allow_nan=Falsecanonical JSON),mareforma._statement(in-toto Statement v1 builder +statement_cidcomputation),mareforma._evidence(stdlib-dataclassEvidenceVectorwith__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 legacycanonical_payloadfor chain-hash + signature inputs; the old shim is removed because it silently desynced from production bytes.
Identity, signing, transparency
- Ed25519 claim signing.
mareforma bootstraponce to generate a keypair at~/.config/mareforma/key(XDG-compliant, mode0600). Everyassert_claimthen signs before INSERT. The signed payload bindsclaim_id,text,classification,generated_by,supports,contradicts,source_name,artifact_hash, andcreated_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 raiseSignedClaimImmutableError.statusandcomparison_summaryremain 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 withtransparency_logged=0and blocks REPLICATED untilgraph.refresh_unsigned()succeeds. - Identity-gated
graph.validate(). The loaded signer must be enrolled in the project’svalidatorstable. The first key opened against a fresh graph auto-enrolls as the root validator (silent self-signed enrollment with aUserWarning). The validation event itself is signed: a DSSE-style envelope binding(claim_id, validator_keyid, validated_at)is persisted to the row’svalidation_signaturecolumn. - New
mareforma validator add/mareforma validator listsubcommands. Each enrollment is signed by the parent validator andis_enrolledwalks 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
BEFOREtriggers enforcePRELIMINARY → REPLICATED → ESTABLISHEDat the storage layer; directPRELIMINARY → ESTABLISHEDis rejected; ESTABLISHED rows requirevalidation_signature. Illegal transitions surface asIllegalStateTransitionErrorwith a parsed<from>-><to>string instead of an opaqueCHECK CONSTRAINT FAILED. - Append-only hash chain. New
claims.prev_hashcolumn carriessha256(prev_chain_link || canonical_payload). UNIQUE partial index +BEGIN IMMEDIATEtogether prevent branched chains from concurrent writers or manual SQL tamper. NewChainIntegrityError. - Cycle / self-loop detection. A claim whose
supports[]would create a cycle (directly or via a chain) raisesCycleDetectedErrorat INSERT and at UPDATE. Forward-walk DFS, depth-capped at 1024 hops. DOI strings insupports[]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 atESTABLISHEDwith a signed seed envelope (payload typeapplication/vnd.mareforma.seed+json, bindsclaim_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_hashparameter onassert_claim(Python API) and--artifact-hashflag onmareforma 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 newartifact_hashcolumn, 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_hashthan the original raisesIdempotencyConflictErrorrather than silently dropping the new hash.
Prompt-safety substrate
mareforma.prompt_safetymodule +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 throughquery_for_llm. Thequery_graphtool 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_claimrunssanitize_for_llm(text)before signing and persisting. Defense in depth: any consumer that readsclaim.textdirectly gets a clean string. Hard cap of 100,000 characters; claims that consist entirely of zero-width / control characters are rejected withValueError.
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'andmare:mediaType='application/x-mareforma-graph+json'. Theusedkey on source-bearing claims was renamed tousedSource(aliased tomare:usedSource). EverySIGNED_FIELDSmember is always emitted on each claim node so downstream consumers (e.g. the bundle verifier below) can re-derivecanonical_payloadfrom a node alone. - SCITT-style signed bundle. New
mareforma export --bundleproduces an in-toto Statement v1 wrapper around the JSON-LD export, withpredicateType='urn:mareforma:predicate:epistemic-graph:v1'and a DSSE-style signature over the whole bundle. Subject names use theurn:mareforma:claim:<uuid>namespace; URN (not DNS) avoids a perpetual-ownership commitment onmareforma.dev. Newmareforma verify <bundle.json>checks the DSSE signature AND every per-claim subject digest. NewBundleVerificationErrornames 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 claimunresolved=Trueand 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); pooledhttpx.Clientwith 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_cachetable: 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-publishis SHA-pinned.actions/checkoutandactions/setup-pythonare pinned by commit SHA: closes the tag-squat / maintainer-compromise vector against the Trusted Publishing OIDC token. - New
SECURITY.mddocuments 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, andmareforma-agentare reserved on PyPI as defensive placeholders that raiseImportErrorand point users back to the canonical package.mare-forma/mare_forma/mare.formaare auto-blocked by PyPI’s confusable-name check. - New
.github/CODEOWNERSand.github/dependabot.yml.
Agent surface
mareforma.open()returns anEpistemicGraph: no@transformrequired. 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 freshgraph.dbfromclaims.tomlfor 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()andsearch()gaininclude_unverified: bool = False. PRELIMINARY claims whose signing key is not in the validators table are excluded by default. Result dicts carry derivedvalidator_reputation(count of ESTABLISHED claims signed by the same validator) andgenerator_enrolled(bool).graph.get_validator_reputation()returns the bulk{keyid: count}map.
Full-text search
- FTS5 over claim text. New
claims_ftsvirtual table (unicode61tokenizer, diacritics folded) synced withclaimsvia three INSERT/DELETE/UPDATE-of-text triggers. Newgraph.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-emptygraph.db. Verifies every signature before any row is inserted. NewRestoreErrorwith.kindfield 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_tomlfailure to stderr at ERROR-level (waswarnings.warn, which production loggers routinely suppress). graph.db remains authoritative.
Removed
@transformdecorator andBuildContext: 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 TOMLquery_claims(): read primitive for the epistemic graphdelete_claims_by_generated_by(): delete claims by source agent- Fixed
LangChainAdapterimport path
v0.2.0 - 2026-04-08
mareforma.agent: framework-agnostic agent provenance moduleMareformaObserver: context manager recording agent events tograph.dbLangChainAdapter: LangChain callback handler
v0.1.0 - 2026-03-25
Initial release.@transform decorator, ctx.claim(), mareforma build,
SQLite epistemic graph, claims.toml backup.