contract disclosure · 60-visibility.md · 61-disclosure.md · 62-accountability.md · guide · card
Audit, erasure and the receipt
record is what a read leaves behind. explain answers why a person does or does not
reach a resource. attest turns the record into evidence a third party checks offline.
erase removes a subject or a tenant from the store, and receipt is the signed artifact a
tenant purge hands back. Every digest over a low-entropy identifier here is keyed or salted,
and the result is pseudonymous.
The chain, what appends to it, and what reads it back:
record
What a read leaves behind: the span, the hash-linked audit entry, where telemetry lands, and the key custody a log opens under.
segmenttestEntries append to a numbered segment file in ascendingseq, and a segment closes under one signed root once it holds its chain header’s segment size, 4096 entries by default.chain-headertestheader.json, written before a new chain’s first entry, fixes the chain’s format, its digest,sha256orblake3, and a segment size of at most 65536 entries; the first v1 entry’sprev_hashis the canonical header’s digest.header-unsupportedtestOpening or verifying a chain whose header names another format or digest, a segment size outsidechain-header, or any field beyondChainHeaderraisesAuditHeaderUnsupported.Why
because a verifier guessing at a digest it does not implement checks nothingentry-formattestA v1AuditEntrycarriesformat: 1, and itsentry_hashis the chain header’s digest over the RFC 8785 canonical JSON of the entry’sformat,seq,prev_hashandattributes.entry-fieldstestVerifying a chain holding an entry with any field beyondAuditEntryraisesbroken-chainat that entry.Why
because the entry digest covers onlyformat,seq,prev_hashandattributes, so without this an injected field verifies cleaninexact-integertestAppending a v1 entry whose attributes hold an integer beyond ±(2^53 − 1) raisesAuditAttributeInexactand appends nothing; verifying a chain holding such an entry raisesbroken-chain.Why
because RFC 8785 writes every number as an IEEE 754 double, so integers past 2^53 share a digest and an edit between two of them verifiesv0-chaintestA chain holding entries withoutheader.jsonis a v0 chain: it verifies and appends under v0 rules, each entry digest overseq,prev_hashand attributes and each root its segment’s last entry digest.group-committestConcurrent appends form one append group, which issues 1 sync of its segment file between segment opens; every member returns after that sync, or every member raisesunpersisted-entry.segment-opentestAn append group that creates a segment file adds 1 sync of the segments directory.tip-signingtestThe signed tip writes at segment close, once the log sees no append for 1 s, and at export; an append group closing no segment writes no tip.unpersisted-entrytestA read whose entry fails to reach local durable storage raisesAuditEntryUnpersistedand returns no rows.unpersisted-wiretestThe read face answersunpersisted-entryin-band as wire codeaudit_entry_unpersisted, HTTP503, with norowsin the result.Why
because a storage fault is the server’s and passes, so a client retries it rather than rewording the readread-entrytestEach read tool call the face answers with a result, over stdio or HTTP, appends one entry, and the result leaves only after that entry’s append group syncs;initialize,pingandtools/listappend none.read-attributestestA read’s entry carriescontextful.tool,contextful.credential,contextful.subject.<member>andcontextful.subject.attestation.<member>per present subject member,contextful.result.rows,contextful.read.at,contextful.read.outcomeandcontextful.tables, the relations the read names.Why
because the chain answers who read how much under which credential, and the attestation keeps an asserted member from reading as identitymemory-read-tablestestAmemory.recallormemory.explainentry’scontextful.tableslists each relation the read ran a statement on under the caller’s session: the entities, the claims, every evidence table its gate read, and a labels table.Why
because the evidence gate reads source rows on the caller’s behalf, and a read its entry omits is one no owner can account forreads-viewtestaudit queryruns one statement overaudit_reads, one row per table a read entry names —read_at,on_behalf_of,agent,table_name,policy,outcome,row_count,claims— computed from the chain’s segments on each call.reads-view-typestestInaudit_reads,row_countis an integer, andclaimslists the claim ids a read entry records, null where it records none.Why
because a count compared as text sorts wrong, and an empty list beside null gives one absence two spellingserasures-viewtestaudit queryexposesaudit_erasuresfrom canonical erasure entries:transaction_id,subject_hash,affected_countsandexecuted_at; the projection retains no state and exposes no selector or row payload.bound-entrytestA bounded read’sread-attributesentry addscontextful.boundsasechoechoes it, the snapshot id the read resolved, the query text and the response’s SHA-256 digest.Why
because a backtest judges the path a case took, and bounds absent from the trajectory prove only the configurationrefused-readtestA read enforcement refuses appends one entry withoutcomerefused, naming the relations the guard parsed, and releases no rows.projectiontestThe projection answers which agent read which table under which policy across a rolling 24 h window, within 1 s.
custody
One append group holds the log at a time, under unanchored or signed custody.
read-chaintestcontextful serve,contextful mcpandcallopen.contextful/audit/before answering; without an explicit startup signing port they use unanchored custody. A chain that does not open stops the process before any row reads.read-signertestAn explicit startup read signing port matches independently configured current public pins before opening and before each append; missing, wrong or retired signing authority refuses held-chain reads throughunpersisted-entrywithout inferring keys from requests or receipts.single-writertestOne append group holds a directory’s audit log at a time: it takesaudit.lock, links after the chain end, syncs, then releases the lock, so several processes append to one linear chain.audit-keytestExplicit erasure resolves one independently random, durable 32-byte project audit key at.contextful/audit.key; concurrent creation preserves one key. Malformed, substituted or unreadable key files followrecovery; Unix creation excludes group and other permissions.audit-key-losstestA published erasure frontier with no persisted project audit key followsrecoverybefore key creation; erasure never regenerates a lost pseudonym key.foreign-tailtestAn append group finding the last segment file changed since its handle’s last write reads that segment’s last entry before linking, and issues no sync beyondgroup-commit.read-onlytestA read-only audit handle verifies the chain without the writer lock; an append through it raisesAuditLogReadOnly.unanchored-over-signedtestAn unanchored handle links entries under an unsigned tip and writes no root; opening one over a signed tip, a signed root or the signedchain.helda held open writes raisesAuditLogAnchored.held-under-appendtestAn unanchored handle’s append group or tip write findingchain.heldwritten since its open raisesunanchored-over-signedand writes nothing, so a held open never meets an unsigned tip or an unrooted segment.Why
because the lock is held per append group, so a held open can land between two groups of a running unanchored handleunsigned-tiptestA held open or signed check over a chain carrying nochain.heldor signed root, whose tip is unsigned, raisesAuditLogUnanchored; anchoring through the signing port signs that chain’s missing roots and its tip.signed-readtestA read verb’s--issuer-keyopens the chain held without anchoring it; over a chain carrying nochain.heldit refuses asunsigned-tipdoes, naminganchor-verb, and appends nothing.Why
because anchoring is one-way andunanchored-over-signedthen refuses every unsigned reader, so only the owner’s explicit verb performs it
telemetry
Spans leave the process over OTLP only when an endpoint is configured.
trace-carriertestA well-formed W3Ctraceparentnames the trace and the parent of the operation’s span; a malformed one is ignored and the operation starts a fresh trace.Why
because a caller’s broken header must cost it the link to its trace, never the operationface-carriertesttesttestA read tool call takestrace-carrierfrom its HTTP request’straceparentandtracestateheaders, or fromparams._meta.traceparentandparams._meta.tracestate; a_metatraceparentwins over the header.Why
because_metatravels with the one message it traces, while a header can belong to a proxy’s connectionread-spantesttesttestWith an exporter configured, each read tool call exports onecontextful.readspan carryingcontextful.tool,contextful.tables,contextful.result.rows,contextful.read.outcome, a refused or failed read’s error identifier aserror.type, and the statement’s SHA-256 ascontextful.query.hash.Why
because a host tracing its own requests sees the substrate’s read as a child of its span, and the digest joins it to the audit entryread-span-contenttesttesttestA read span carries no statement text, subject member or row value.Why
because a collector sits outside the audit chain’s custody, so a span discloses only digests, counts and identifiersspan-exporttestWithOTEL_EXPORTER_OTLP_ENDPOINTset, spans leave over OTLP/HTTP to its/v1/tracespath, or toOTEL_EXPORTER_OTLP_TRACES_ENDPOINTas given, carrying theOTEL_EXPORTER_OTLP_HEADERSheaders andOTEL_SERVICE_NAMEasservice.name.span-encodingtesttesttesttestA build linking theotlpfeature sends each batch as an OTLP protobufExportTraceServiceRequestunderContent-Type: application/x-protobuf; a build without it sends the OTLP JSON mapping underapplication/json.Why
because a collector that accepts no OTLP JSON still receives the spans of a build linkingotlpspan-unconfiguredtestWith neither endpoint set, no exporter exists: no span leaves the process and telemetry opens no socket.Why
because egress reachable under a default configuration is telemetry the operator never chosespan-queuetestSpans leave off the operation’s path through a queue of 2048 spans; a span past the cap, or lost to a failed export, is dropped and counted.Why
because an unreachable collector must add no latency to the read or run that made the spanstage-spantesttesttestWith an exporter configured, every run, each seed chunk run included, exports one child span of itsrun-spanper pull, landing, commit and journaled step, namedcontextful.pull,contextful.land,contextful.commitorcontextful.step.Why
because a slow or failed run shows the stage that held it on the trace its caller already readsstage-attributestesttestA stage span carriescontextful.stage, the run’scontextful.table, a pull’s or step’scontextful.steplabel,contextful.outcome, a failed stage’s error identifier aserror.type, and thecontextful.rowsorcontextful.bytesthe stage counts.Why
because the stage, its outcome and its count locate a failure without opening the storestage-outcometestA pull, landing or commit recordsokorfailedas its outcome; a journaled step recordsrecorded,replayed,unrecorded,waitingorfailed.Why
because a replayed step made no source call, and a trace reading it as a pull misstates the run’s egressstage-contenttesttestA stage span carries no row value and no statement text.Why
because a collector sits outside the store’s custody, so a stage span discloses only names, counts and identifiersstage-unconfiguredtesttestWith no exporter configured, a run times no stage and builds no stage span.Why
because span work no collector receives is cost the operator never choseroute-carriertesttesttestEvery HTTP route but/mcp,/healthand/auth/exchange— receive, receive outcome, run stop, run stream, awake, control, claim write and connection routes — takestrace-carrierfrom its request’straceparentandtracestateheaders.Why
because a run or read a request starts belongs to its caller’s trace whichever route carries itroute-spantesttestWith an exporter configured, each such request exports one server span named by its method and route template, carryinghttp.request.method,http.route,http.response.status_code, and for a 4xx or 5xx answer its error identifier, else its status, aserror.type.Why
because the route a request took and its answer join the caller’s trace as the tool call’s read span doesroute-parenttesttestA route’s handler receives atraceparentnaming the route span, so a run the receive route lands takes the request’s trace and parents its run span under the route span.Why
because the landing is the request’s work, and a run rooted apart from it hides that delivery’s costqueued-parenttestA deliveryreceive-queued-askqueues carries its route span’s trace on its message, so each run a consumer lands for it parents its run span under that route span; a group run continues its first member’s trace.Why
because a queued landing is the request’s deferred work, and a run rooted in the consumer’s trace hides that delivery’s costqueued-contenttesttestA queued message’s trace is the route span’s W3Ctraceparentandtracestatealone, never another request header, a path segment’s value or the query.Why
because a managed queue’s operator reads every message, and a trace needs nothing beyond its two W3C carriersroute-contenttestA route span records the route template and never a path segment’s value, the query or the body.Why
because a path segment can hold an awakeable token or a batch key, which no collector is grantedjob-carriertestAn import or apply under a trace context, fromTRACEPARENTandTRACESTATEor the/control/applyroute span, records its W3Ctraceparentandtracestatealone with each job it adds or changes; an unchanged job keeps its carrier.Why
because a job’s submitter is the apply that set its declaration, and the two W3C carriers are all a fire needs to reach that spanjob-linktestA fire of an applied version’s job exports its execution’s run span with one span link to the carrier that version records with the job; a job recorded with none links nothing.Why
because a link joins a fire to its submitter’s trace without placing the fire’s duration inside itscheduled-tracetestServe dispatches each scheduled job fire withoutTRACEPARENTorTRACESTATE, so every fire starts a fresh trace that onlyjob-linkjoins to its submitter.Why
because one trace holding every fire of a recurring job grows without bound past a collector’s trace window, while a fresh trace per fire keeps each fire whole
explain
The access decision and its path, replay over a window with its coverage, and the audience report.
decisiontestaudit explain --table <t> --subject <principal>answersadmitwhen a grant the exchange policy issues for the--rolenames, else itsdefault_grants, carriesreadover a pattern covering the table, anddenyotherwise.pathtestA decision’s path names the role or default set, the covering grant, then each step the table declares inrelation-order; a deny’s path ends atdefault-deny.replaytest--window <from>..<to>replays each chain entry naming the subject and the table whose read instant falls in the window: its seq, instant, outcome, agent and recordedcontextful.*attributes.coveragetestAn explanation carries a coverage block: segments and entries verified, entries carrying no read instant, window spans the chain does not reach, and each visibility binding’s source with its sweep watermark.audiencetestaudit explain --audience <t>lists by name the roles whose grants read the table, whetherdefault_grantsdo, and peron_behalf_ofscheme the count of principals and served reads.no-rowtestAn explanation returning a row, field value or excerpt from the resource it decides about, a replayedcontextful.result.*attribute beyondcontextful.result.rowsincluded, raisesVisibilityDiagnosticRow.Why
P2empty-windowtestA window holding no observations raisesVisibilityNoObservationsand states that no claim is available.Why
P2unqualified-assurancetestA negative assurance answer emitted without its coverage block raisesVisibilityUnqualifiedAssurance.Why
P2groups-not-memberstestA reader-facing explanation rendering the member identities of a group on the path raisesVisibilityIndividualNamed.Why
P2
attest
Chain verification, signed segment roots, lineage attestations, and the reach of each guarantee.
broken-chaintestA disagreeing digest, entry format or Merkle root, a sequence gap, a failing signature, or, besidechain.tip,chain.heldor a signed root, an absent chain or a missing or unsigned tip raisesAuditChainBrokenat the earliest failing index.merkle-roottestA v1 segment root is the RFC 6962 Merkle tree hash, under the header’s digest, over the segment’s raw entry digests inseqorder.root-tagtestA v1 signed root carries its signing algorithm,Ed25519orES256, and signs it with the header digest, segment number, entry count and root.inclusion-prooftestprovereturns one entry, its chain header, its RFC 6962 audit path and its segment’s signed root; the proof verifies offline under the signer’s public key alone.proof-invalidtestA proof whose entry, audit path, header, root or root signature disagrees raisesAuditProofInvalid.root-replicationtestaudit anchoronce it signs, and a held chain’s replicator at start and every 10 min, copy each signed root the[sync]bucket lacks to<prefix>/<project>/audit/roots/<segment>.json; a failed copy waits for the next and fails no anchor, append or read.replica-verifytestaudit verify --bucketalso holds the chain to each replicated root: a root failing its signature, closing a segment the chain does not reach, or absent or different locally breaks the chain asbroken-chaindoes.Why
because deleting the roots,chain.heldand the tip’s signature leaves a chain no local check tells from an unanchored onereplicate-verbtestaudit replicatecopies the rootsroot-replicationcopies, once, and prints the segments it sent.lineage-elisiontestA lineage attestation counts a claim’s evidence references into tables the caller’s session does not register aswithheldin thecontextful.recallblock, and names no such table.Why
because naming a withheld table leaks its existence outside the caller’s authority, while a count keeps the suppression explicable and never confirms a table existsverify-verbtestaudit verifyopens the chain read-only and prints its end when every check ofbroken-chainpasses, each signature included under--public-key.anchor-verbtestaudit anchor --issuer-key <seed>signs the chain’s missing roots and its tip asunsigned-tipanchors them, and prints the chain end.prove-verbtestaudit prove --seq <n>prints the proofinclusion-proofreturns, andaudit check-proofverifies one under a public key, reading no store.owner-onlytestAn audit verb reading a chain, presented with a capability credential, raisesAuditRequiresOwnerbefore it reads an audit file.Why
because the chain names every person’s reads, and a credential scoped to some tables reaches no record of others
erase
The one erasure verb: subject tombstones, the bounded provenance cascade, and the tenant rewrite.
privilegetestErasure sits outside the default grant set, and a caller without the forget grant raisesErasureUngranted.unsupported-scopetestAn empty target set or erasure admitted only by narrowed grants raisesErasureScopeUnsupportedbefore store access; supported erasure admits every requested table explicitly.Why
because ignoring a grant constraint widens destructive authoritysubject-columntestA table names its subject column assubject_id = "<column>"under[[pipeline.tables]]. A subject erasure against a table without one raisesErasureSubjectUndeclared, naming the table.key-settestA key-set request binds its selectors to the declared keys oferasure-metadataand carries an opaque subject hash instead of a raw subject identity.column-key-settestA column-scopedkey-setcarries a declared column and non-null scalar keys; canonical declarations select every table naming that column, including tables without subject metadata. Caller table subsets and malformed selectors followunsupported-scope.Why
because a caller table subset leaves declared copies of the erased key readableatomic-publicationtestSubject and key-set erasure select one committed frontier across every affected table; a failed transaction exposes none of its staged replacements.reader-frontiertestQuery, file, retrieval and cache responses resolve one erasure frontier and revalidate it before releasing bytes; an obsolete frontier releases no rows or files.surviving-citationstestA citing table carrying the survival declaration oferasure-metadataretains its rows; its references resolve througherased-verdict.subject-pseudonymtestSubject erasure computes its subject hash as a domain-separated HMAC-SHA-256 under the configured project audit key; no transaction or audit artifact carries raw subject identities or erased key values.signer-bindingtestErasure signs through the configured issuer or node’ssigning-port; readers verify the canonical audit chain under configured public-key pins, independently of transaction artifacts. Missing signing or verifier configuration followsrecovery.row-free-audittestThe signed audit chain binds the committed transaction,subject-pseudonymand affected counts to the exact replacement-file inventory.store-bindingtestThe signed erasure entry bindsstore-identity; readers reject a frontier whose signed store identity differs from the receiving store, followingrecovery.working-publicationtestErasure preserves an immutable signed survivor baseline and selects independent working copies under the ordinary table layout; normal append and fold advance working publication without changing the baseline or requiring another erasure signature.recoverytestRecovery completes collection for a committed frontier and discards uncommitted replacements; an incomplete or disagreeing transaction record raisesErasureTransactionIncompletebefore releasing store content.recovery-verbtestcontext erase --recover --project <project>resumesrecoveryunderprivilegeandeffect-boundarybefore collection; selectors and signing ports are absent.unsigned-recoverytestUncommitted staging origins resolve against canonical stored tables and require Forget over the complete store scope; signed-only recovery admits the complete authenticated frontier. Uncommitted recovery returns no transaction identity.retirement-inventorytestSigned retirement inventories bind regular paths through domain-separated HMAC underaudit-keyand their file digests; recovery admits missing files as completed collection, validates all remaining files before deletion, and rejects changes throughrecovery.canonical-audit-keytestErasure and keyed retirement recovery require the persisted canonicalaudit-key; caller-supplied keys equal it, and absent or mismatched keys followrecoverybefore content access, signed intent, staging or collection.survivor-namestestErasure survivor snapshots use row-independent part names, preserve admitted surviving rows, record rewritten part key versions, and rebuild declared indexes throughvector-by-foldandfulltext-by-fold.cascadetestIn the same operation the cascade invalidates every derived fact whose provenance reaches the subject key or a tombstoned identifier within 16 hops.cascade-unboundedtestA provenance chain deeper than the cascade bound raisesErasureCascadeUnbounded, and the erasure commits nothing.physical-removaltestWithin 24 h of the erasure’s commit, every file holding an erased row, superseded snapshots insideretentionincluded, is rewritten or collected.tenant-column-typetestA tenant column of a non-string type raisesPurgeTenantColumnType.tenant-undeclaredtestA purge finding no model declaring an outermost partition key raisesPurgeTenantUndeclared.owner-onlytestA purge presented with a capability token raisesPurgeRequiresOwner, evaluated before anything reveals store contents.tenant-rewritetestcontext erase --tenant <id>rewrites every row whose bare outermost partition column holds the identifier’s exact bytes out of every table throughatomic-publication, and prints a freshly signed receipt.
dead letters
Erasure reaches the <table>_dead_letter beside each declared table by the evidence its letters cite, or purges the letters citing none.
dead-letterstesttesttestErasure also rewrites each row of a declared table’s<table>_dead_letterwhosedead-letter-evidencecites a removed row; undecodableevidencefollowsunsupported-scope.Why
because a dead letter repeats its source rows’ content, so a letter left in place keeps erased bytes readabledead-letter-granttestA Forget grant covering a table also covers its<table>_dead_letter, in erasure and inrecovery-verb.Why
because a dead-letter table derives from its table alone, so covering it reaches no content the grant does not already reachdead-letters-without-evidencetesttesttesttesttestcontext erase --dead-letters-without-evidence, alone or beside a selector, removes each<table>_dead_letterrow withoutevidencefor every requested table, underdead-letter-grantandatomic-publication.Why
because a letter written without evidence cites no row, so no selector reaches it, and an unsigned deletion leaves no audit entry or physical-removal guaranteepurge-countstesttestThe audit entry and receipt ofdead-letters-without-evidencecount its removed rows per dead-letter table, apart fromaffected_counts; given alone, the entry’s subject hash names the operation instead.Why
because an operator proves the purge from its signed entry, and a purge without a subject has no subject to pseudonymize
unsettled: Is request-to-last-replica erasure within 72 h an engine bound or an operator objective, given the engine schedules no replica refresh? owner: disclosure affects: disclosure.erase
unsettled: Does an erasure refuse fact reads, the free-form statement face included, until re-synthesis restores the facts the cascade invalidated? owner: disclosure affects: disclosure.erase
receipt
The signed artifact a tenant purge returns: its claim, its coverage block, its pseudonym and its idempotence.
pseudonymtesttenant_hashis SHA-256 over a random per-receiptsaltand the tenant identifier. A receipt carrying the identifier in cleartext raisesReceiptIdentifierLeakbefore signing.widened-claimtestA coverage block naming a scope broader than itsreceipt_versiondeclares raisesReceiptClaimWidened.replayedtestReturning the byte-identical earlier artifact in place of a freshly signed one raisesReceiptReplayed.coveragetestAreceipt_version1 coverage block covers the canonical store and names replicas and the replication bucket as excluded.Why
because a purge deletes no bucket object, so a claim over the replication bucket names a removal that never ranchain-recordtestA purge appends its receipt’srequest_idand SHA-256 digest to the audit chain after signing; a digest the chain already holds followsreplayed.prior-requesttestA purge’s receipt entry records the tenant’s HMAC-SHA-256 under the project audit key beside itsrequest_id; a later purge of that tenant signs the latest suchrequest_idasprior_request_id.Why
because the audit chain already orders every receipt, and a keyed digest links repeat purges without a cleartext identifier or a second recordzero-rowstestA purge whose tenant holds no retained row rewrites nothing and signs a receipt with a zero count for every declared table and an emptytransaction_id.Why
because an owner proves a tenant’s absence with one artifact whether or not rows existed
Shapes
The audit segment and the erasure transaction records:
.contextful/
audit/
header.json {format, digest, segment_entries}; absent on a v0 chain
segments/
000001.jsonl one entry per line, seq ascending
000001.root.json the signed Merkle root closing the segment
chain.tip last accepted {seq, entry_hash}
chain.held signed {seq, entry_hash} where the issuer first held the log
audit.lock held by the one append group writing
audit.key the 32-byte project audit key
context/<project>/
_erasure_frontier.json {transaction_id, audit_hash, audit_seq, tables}
forget/
<subject_hash>.stale {subject_hash, executed_at}
_erasure/
staging/<id>/ uncommitted replacements, discarded on recovery
committed/<id>/ signed survivor baselines and certificates
working/<id>/ working copies that append and fold advance
A chain header:
{ "format": 1, "digest": "sha256", "segment_entries": 4096 }
One v1 entry, its prev_hash the header’s digest:
{
"format": 1,
"seq": 1,
"prev_hash": "sha256:a36fc99c...",
"attributes": {
"contextful.query.hash": "hmac-sha256:9f2c...",
"contextful.subject.on_behalf_of": "user://ada",
"contextful.subject.attestation.on_behalf_of": "verified",
"contextful.tables": ["orders", "customers"],
"contextful.row.phi_columns_returned": 0,
"contextful.result.rows": 42
},
"entry_hash": "sha256:1b7e..."
}
A v1 root, and an inclusion proof of one entry:
{
"format": 1,
"alg": "Ed25519",
"header": "sha256:a36fc99c...",
"segment": 1,
"root": "sha256:a4f0...",
"count": 4096,
"signature": "9e41..."
}
{ "header": { "format": 1, "digest": "sha256", "segment_entries": 4096 },
"entry": { "format": 1, "seq": 1, "...": "..." },
"path": ["3f9c...", "..."],
"root": { "format": 1, "segment": 1, "...": "..." } }
A purge receipt:
{
"receipt_version": 1,
"coverage": {
"claim": "rewrite-and-exclude",
"includes": ["run-files", "snapshots", "model-builds", "memory-mirror"],
"excludes": ["replication-bucket", "replicas", "physical-destruction"]
},
"salt": "b64:Qm9n...",
"tenant_hash": "sha256:4d1a...",
"request_id": "prg_01HQ8F3K2M",
"completed_at": "<rfc3339-utc>",
"tables": [{ "name": "orders", "rows_removed": 118422, "files_rewritten": 37, "builds_rewritten": ["bld_7a21"] }],
"cascade": [{ "name": "memory_facts", "rows_removed": 903 }],
"prior_request_id": null,
"signature": { "payload_hash": "sha256:c0ff...", "signature": "3045a1...", "public_key": "ed25519:9a7d..." }
}