Docs / Specification
A-disclosure — Visibility, disclosure and accountability decisions
Status: accepted
mirrored-authorization-carries — Mirrored authorization carries a freshness budget and refuses past it
Cited bydisclosure.sweep.coverage-watermarkdisclosure.sweep.coverage-watermarkA sweep run that failed or skipped any governed resource of its source and advances watermark_at raises VisibilityWatermarkUncovered, and the watermark stays where it stood.disclosure.sweep.ungapped-streamdisclosure.sweep.ungapped-streamAdvancing watermark_at from an event stream the mapping has not declared gap-detectable raises VisibilityUngappedStream.disclosure.sweep.sweep-epochdisclosure.sweep.sweep-epochA committed run advances its source's epoch by one.disclosure.reach.degraded-uncacheddisclosure.reach.degraded-uncachedRetaining a reachable set or result produced past a budget, or under the narrowing posture, raises VisibilityDegradedCached.disclosure.reach.cache-epochsdisclosure.reach.cache-epochsThe reachable-set cache keys on the subject, the directory epoch and the epoch of each source the read touches, so a sweep invalidates only sets reading its source.disclosure.bound-staleness.budget-grammardisclosure.bound-staleness.budget-grammarmax_acl_staleness is a positive integer with one suffix from s, m, h, d. A compound form, a bare number or another suffix raises VisibilityBudgetMalformed, naming the table and the text read.disclosure.bound-staleness.access-staledisclosure.bound-staleness.access-staleA read raises VisibilityAccessStale (HTTP 503, carrying source, lag, budget and last observation) when a touched table's source lag exceeds its budget, the source has no completed full run, or the public-status watermark is past budget.disclosure.bound-staleness.budget-below-cadencedisclosure.bound-staleness.budget-below-cadenceA budget tighter than the sweep cadence its source sustains raises VisibilityBudgetUnreachable at diagnose, naming both figures.
A mirrored grant is only as current as the oldest resource its sweep observed. The narrowing posture on_stale = public_only serves only swept public rows, marked degraded.
| Option | Lost on | Cost |
|---|
| Coverage watermark, per-table budget, typed refusal (chosen) | — | A limping sweep takes a source offline; the public-status sweep is a second shape to operate. |
| Serve past the budget with an advisory field | Detectability | A revoked grant keeps answering, marked by a field nobody reads. |
| Zero rows past the budget, or a truncated closure | Detectability | A shortened set reads as a quiet corpus. |
| A time-to-live on the reachable-set cache | Invalidation by construction | A revoked grant answers for an undeclared second staleness window. |
Revisit: a source offers sequenced delivery with gap reconciliation.
fidelity-bounded-what — Fidelity is bounded by what the source enforces
Cited bydisclosure.declare-fidelity.family-bounddisclosure.declare-fidelity.family-boundA person-container table at a servable level, or a directory table above excluded, raises VisibilityFamilyBound, and the manifest declaring it does not load.disclosure.declare-fidelity.family-undeclareddisclosure.declare-fidelity.family-undeclaredA mapping landing a table without naming its family raises VisibilityFamilyUndeclared.disclosure.declare-fidelity.federated-registereddisclosure.declare-fidelity.federated-registeredRegistering a federated table on a read face raises VisibilityFederatedRegistered.disclosure.pack.mapping-absentdisclosure.pack.mapping-absentA pack landing a table with neither an access mapping nor excluded raises VisibilityMappingAbsent at diagnose, naming the pack and the table.disclosure.pack.asserts-accessdisclosure.pack.asserts-accessAn access-shaped key the mapping schema does not define raises VisibilityPackAssertsAccess by name. Fields outside the mapping are fetch inputs and reach no authorization decision.disclosure.pack.unsupported-coarsedisclosure.pack.unsupported-coarseDeclaring coarse where the source emits no container or workspace signal raises VisibilityUnsupportedCoarse.
The family is the one judgment per source. A source that computes access answers live under the reader’s delegated credential; a source with no permission data lands with no fidelity claim.
| Option | Lost on | Cost |
|---|
| Per-table level bounded by a declared family (chosen) | — | Federated legs pay live latency and go dark with the source. |
| One deployment-level permission-aware claim | Falsifiability | The weakest source sets the real guarantee undetected. |
| Mirror a sharing engine’s inputs and reproduce its rules | Drift | A second evaluator disagrees in exactly the restricting constructs. |
| An audience field in the pack | Enforceability | No read-path step consults it. |
grants-rows-under — Grants are rows under _visibility, and the closure is a rebuilt projection
Cited bydisclosure.mirror.grants-tabledisclosure.mirror.grants-tableA sweep lands a source's access data as ordinary rows of _visibility/<source>/resources, grants, principals, group_members, identity_links, tombstones and freshness, through the land path and its write lock.disclosure.mirror.unmirrorable-openingdisclosure.mirror.unmirrorable-openingA resource whose access list the sweep could not mirror opens only through a grant row the store owner lands carrying expires_at; the row confers nothing past that instant, and no manifest key opens a resource.disclosure.reach.closure-overflowdisclosure.reach.closure-overflowA closure reaching its depth or node bound raises VisibilityClosureDepth with the non-retryable HTTP status 422 and returns no set.disclosure.template.read-envelopedisclosure.template.read-envelopeA template read returns rows, truncation, row ceiling, suppression presence and the disclosure policy hash; set-valued policy fields follow disclosure-digest at every nesting depth.
Authorization has one source, the join over observed rows, and a replica answers offline.
| Option | Lost on | Cost |
|---|
Grant rows via the land path, closure in derived.sqlite (chosen) | — | A replica rebuilds the closure after each pull. |
| A separate grant store beside the catalog | One write path | A second commit protocol, lock and erasure path. |
| A closure synced as files | One authorization source | Closure bytes on a replica drift from their rows. |
| Openings as manifest policy | Enforceability | A key no sweep observes answers without expiry or record. |
aggregate-disclosure-enforced — Aggregate disclosure is enforced at build with noisy thresholding and an up-front budget reservation
Cited bydisclosure.release.budget-reservationdisclosure.release.budget-reservationBefore computing, a release reserves each contributing unit's declared per-run spend against its lifetime cap in one catalog transaction. A failed reservation raises DisclosureUnitBudgetExhausted, and the release publishes nothing.disclosure.release.release-envelopedisclosure.release.release-envelopeA release reports its published table, policy hash, reserved per-run spend, suppressed-group presence and row count without carrying a contributor key.disclosure.release.duplicate-graindisclosure.release.duplicate-grainA non-numeric, non-grain column rides the grouping, and one not constant within its group raises DisclosureDuplicateGrain.disclosure.release.group-keydisclosure.release.group-keyA group key not a subset of the permitted grouping columns raises DisclosureGroupKeyNotPermitted.disclosure.release.contributor-keydisclosure.release.contributor-keyA governed statement omitting the contributor key raises DisclosureContributorKeyAbsent.disclosure.release.forbidden-columndisclosure.release.forbidden-columnA contract declaring a forbidden column, or a materialization producing one, raises DisclosureForbiddenColumnPublished. The check runs after the contributor key leaves the published columns.disclosure.suppress.min-group-sizedisclosure.suppress.min-group-sizemin_group_size counts distinct contributors and is at least 2 subjects. A group whose noised distinct-contributor count falls under it is suppressed, and a smaller declared value raises DisclosureMinGroupSizeBelowFloor.disclosure.suppress.contributor-sharedisclosure.suppress.contributor-sharemax_contributor_share lies above 0 percent and at most 100 percent of a group's sign-insensitive metric mass; a value outside raises DisclosureShareOutOfRange.disclosure.suppress.dominance-unverifiabledisclosure.suppress.dominance-unverifiableA group under a share constraint whose per-contributor masses are unavailable raises DisclosureDominanceUnverifiable and is suppressed.disclosure.suppress.empty-policydisclosure.suppress.empty-policyA policy setting neither threshold, or only a share ceiling at a whole group's mass, raises DisclosurePolicySuppressesNothing.disclosure.suppress.grouping-allowlistdisclosure.suppress.grouping-allowlistAn empty permitted-grouping list, or a permitted name that is not column-shaped, raises DisclosureGroupingAllowlistEmpty.disclosure.suppress.sentinel-rowdisclosure.suppress.sentinel-rowA release with emit_sentinel = true represents every suppressed group together as one __suppressed__ row carrying no group key or exact figure.disclosure.bound-cohort.singleton-cohortdisclosure.bound-cohort.singleton-cohortA cohort model whose permitted grouping columns or non-contributor grain columns name its contributor key or a forbidden column raises DisclosureSingletonCohort at declaration, before any row lands under it.disclosure.bound-cohort.exact-figuresdisclosure.bound-cohort.exact-figuresA policy declaring figures = "exact" on a per-person table raises DisclosureExactPerPerson. figures defaults to "noised", and exact figures publish only from a cohort table.
Every read path inherits the guarantee from the staged bytes.
| Option | Lost on | Cost |
|---|
| Build-time enforcement, reservation up front (chosen) | — | A policy edit is a rebuild; an exhausted unit blocks every release naming it. |
| Suppress on exact counts, or a switch to exact figures | Privacy guarantee | Cell presence discloses a threshold crossing; a policy edit removes the protection. |
| Debit spend after the release | Concurrency | Two concurrent releases each spend budget the other observes. |
| A query-time aggregate evaluator | Survival across read paths | Reimplemented per surface; a rollup loses per-contributor mass. |
Revisit: lookalike segment release settles its audience floor, readback rule and cross-party consent.
exact-integer-noise — Exact integer noise spends the declared budget across released statistics
Cited bydisclosure.release.noise-mechanismdisclosure.release.noise-mechanismA noised release draws exact two-sided geometric noise scaled to each statistic's sensitivity for distinct-contributor counts and fixed-point sums; the per-run epsilon is divided among released statistics.disclosure.release.metric-boundsdisclosure.release.metric-boundsA noised sum folds each contributor to one value, clips that total to declared finite bounds and rounds it to a positive quantum; absent bounds or quantum raise DisclosureMetricBoundsAbsent.disclosure.release.lookalike-refusaldisclosure.release.lookalike-refusalA release requesting an activation handle or lookalike segment, as --output activation-handle or --output lookalike-segment, raises DisclosureLookalikeUnsupported and publishes nothing.
An auditable privacy spend needs finite sensitivity and a sampler with no floating-point tails.
| Option | Lost on | Cost |
|---|
| Exact geometric noise, bounded metrics, no lookalike release (chosen) | — | Every noised sum needs bounds; an activation product has no release path. |
| Floating-point Laplace noise over raw sums | Auditable sensitivity | The privacy spend is unverifiable. |
| Exact grouped figures behind a noisy threshold | Figure privacy | A published sum reveals contributors even with the key hidden. |
| Lookalike release beside aggregate figures | Consent | An activation handle reaches a party under no consent rule. |
Revisit: a consent and readback contract admits activation handles.
model-declares-spend — The model declares the spend, the project the cap, and the ledger stays on the machine
Cited bydisclosure.release.model-policydisclosure.release.model-policyA published model-block governed by aggregate disclosure declares [model.disclosure]; metric_bounds names each noised sum's lower bound, upper bound and quantum.disclosure.release.release-verbdisclosure.release.release-verbcontextful disclosure release <model> --project <project> builds the named model from its declared statement, applies its disclosure policy, and publishes one derived table.disclosure.release.spend-declarationdisclosure.release.spend-declaration[model.disclosure] declares per_run_spend, the positive epsilon one release reserves from each contributing unit; a release whose policy declares none raises DisclosureSpendUndeclared and publishes nothing.disclosure.release.lifetime-capdisclosure.release.lifetime-capA project manifest's top-level [disclosure] block declares unit_lifetime_cap, the epsilon each contributing unit spends across every release; a release grants it to each unit it reserves from, and an undeclared cap is zero.disclosure.release.budget-ledgerdisclosure.release.budget-ledgerA project's budget ledger is .contextful/disclosure/<project>/budget.sqlite, outside the synced store root, and records each contributing unit under the keyed digest of its key under the project's pepper.
One cap per unit decided it. No spend is undeclared, and no contributor key rests outside the store.
| Option | Lost on | Cost |
|---|
| Spend on the model, cap on the project, local ledger (chosen) | — | Two machines releasing one project each hold a full cap. |
| A cap per model | One cap per unit | A unit named by two models spends two lifetimes. |
| A default spend when none is declared | Author intent | Every unit pays a budget nobody chose. |
| The ledger inside the store root | Replica safety | A push carries per-contributor balances to every replica. |
Revisit: a project releases from more than one machine.
cross-owner-release — A cross-owner release runs only when the boundary is enforced below the engine
Cited bydisclosure.set-mode.check-verbdisclosure.set-mode.check-verbcontextful disclosure check --config <path> diagnoses each published model's disclosure policy and statement from local files and reports every refusal by model name.disclosure.set-mode.aggregate-shapedisclosure.set-mode.aggregate-shapeA statement is aggregate-shaped when its parsed SELECT groups rows, has DISTINCT or HAVING, or calls an aggregate catalog function outside a window expression.disclosure.set-mode.policy-absentdisclosure.set-mode.policy-absentA published model matching aggregate-shape and carrying neither a disclosure policy nor a recorded opt-out fails the diagnostic with DisclosurePolicyAbsent.disclosure.set-mode.opt-out-recorddisclosure.set-mode.opt-out-recordA model records an opt-out as a nonempty disclosure_opt_out reason; combining it with [model.disclosure] raises spec-invalid.disclosure.set-mode.model-unreadabledisclosure.set-mode.model-unreadableA published model whose sql_file under statement-source does not read raises DisclosureModelUnreadable.disclosure.set-mode.clean-room-preconditionsdisclosure.set-mode.clean-room-preconditionsA cross-owner store carries per-owner signed manifest subtrees, per-owner write prefixes enforced by the object store's access policy, and per-owner signing keys; one missing, checked before any cross-prefix write, raises DisclosureCleanRoomPreconditionUnmet.disclosure.set-mode.hashed-joindisclosure.set-mode.hashed-joinTwo owners match on a key each hashes at write time under a per-pair pepper re-randomized per join; matched rows leave as an aggregate under a group-size floor. A static pepper raises DisclosureStaticPepper.disclosure.set-mode.pepper-versiondisclosure.set-mode.pepper-versionA cross-owner join accepts only rows carrying its pair's current pepper version; rotation re-lands both owners' join keys before the next release.
A protection the engine only documents fails silently, as corrupted data or a forged grant.
| Option | Lost on | Cost |
|---|
| Store-enforced preconditions, per-pair pepper (chosen) | — | Stores without per-prefix write policy are ruled out; pepper escrow is operational work. |
| Protections as operator responsibilities | Detectability | Failure arrives silently. |
| Two of three preconditions | Coverage | The third path stands open. |
| A static pepper shared across joins | Pepper compromise | One recovered pepper reverses every join. |
Revisit: private set intersection replaces the pepper; an attested enclave join serves very-high-risk pairings.
hash-chain-attestable — The hash chain is the attestable record
Cited bydisclosure.record.unpersisted-entrydisclosure.record.unpersisted-entryA read whose entry fails to reach local durable storage raises AuditEntryUnpersisted and returns no rows.disclosure.record.read-chaindisclosure.record.read-chaincontextful serve, contextful mcp and call open .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.disclosure.attest.root-replicationdisclosure.attest.root-replicationaudit anchor once 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.
Audit storage becomes an availability dependency of reads. A compromised node cannot truncate history its replicated roots cover; telemetry is a projection with no durability obligation.
| Option | Lost on | Cost |
|---|
| Linked chain under group commit, signed segment roots (chosen) | — | A read fails when its entry cannot persist. |
| Telemetry spans as the record | Detectability | A dropped span reads as a quiet period. |
| Fail open with a gap marker, or buffer and retry | Evidential completeness | Rows leave before their entry exists. |
| An external transparency log as primary | Air-gapped operability | Some deployments cannot reach a third party. |
Revisit: audit writes replicate to a second store; group commit sustains less than the reference read rate.
append-group-pays — An append group pays one sync, and the tip signs at segment close
Cited bydisclosure.record.group-commitdisclosure.record.group-commitConcurrent 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 raises unpersisted-entry.disclosure.record.segment-opendisclosure.record.segment-openAn append group that creates a segment file adds 1 sync of the segments directory.disclosure.record.tip-signingdisclosure.record.tip-signingThe 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.
A per-append sync of segment, tip and directory, plus a signature, costs three full syncs (F_FULLFSYNC on macOS) per read. Syncs per read decided it; throughput then scales with concurrency.
| Option | Lost on | Cost |
|---|
| One sync per group, tip at close, idle and export (chosen) | — | One failed sync refuses the whole group; the unsigned tail truncates undetected. |
| Three syncs and a signature per append | Syncs per read | Latency grows with every durable write the tip needs. |
| Sync and sign the tip per group | Syncs per read | Three syncs protect a tail no replicated root covers yet. |
Cited bydisclosure.record.chain-headerdisclosure.record.chain-headerheader.json, written before a new chain's first entry, fixes the chain's format, its digest, sha256 or blake3, and a segment size of at most 65536 entries; the first v1 entry's prev_hash is the canonical header's digest.disclosure.record.entry-formatdisclosure.record.entry-formatA v1 AuditEntry carries format: 1, and its entry_hash is the chain header's digest over the RFC 8785 canonical JSON of the entry's format, seq, prev_hash and attributes.disclosure.record.v0-chaindisclosure.record.v0-chainA chain holding entries without header.json is a v0 chain: it verifies and appends under v0 rules, each entry digest over seq, prev_hash and attributes and each root its segment's last entry digest.disclosure.attest.broken-chaindisclosure.attest.broken-chainA disagreeing digest, entry format or Merkle root, a sequence gap, a failing signature, or, beside chain.tip, chain.held or a signed root, an absent chain or a missing or unsigned tip raises AuditChainBroken at the earliest failing index.disclosure.attest.merkle-rootdisclosure.attest.merkle-rootA v1 segment root is the RFC 6962 Merkle tree hash, under the header's digest, over the segment's raw entry digests in seq order.disclosure.attest.root-tagdisclosure.attest.root-tagA v1 signed root carries its signing algorithm, Ed25519 or ES256, and signs it with the header digest, segment number, entry count and root.disclosure.attest.inclusion-proofdisclosure.attest.inclusion-proofprove returns 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.disclosure.attest.proof-invaliddisclosure.attest.proof-invalidA proof whose entry, audit path, header, root or root signature disagrees raises AuditProofInvalid.
A v0 entry proves membership only with its whole segment, and its header fields sit outside the digest. Offline per-entry evidence decided it; every released format verifies forever.
| Option | Lost on | Cost |
|---|
| Versioned whole-entry digest, Merkle root (chosen) | — | Two verifier paths kept forever; attribute integers within ±(2^53 − 1). |
| Keep v0 | Per-entry evidence | Membership needs the whole segment. |
| One digest for every chain | Embedder fit | A verifier pinned to the other digest cannot read the chain. |
| Sign every entry | Syncs per read | One signature per read, which group commit amortizes away. |
audit-log-opens — An audit log opens under the key custody its caller holds
Cited bydisclosure.record.read-signerdisclosure.record.read-signerAn 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 through unpersisted-entry without inferring keys from requests or receipts.disclosure.record.audit-key-lossdisclosure.record.audit-key-lossA published erasure frontier with no persisted project audit key follows recovery before key creation; erasure never regenerates a lost pseudonym key.disclosure.record.unanchored-over-signeddisclosure.record.unanchored-over-signedAn unanchored handle links entries under an unsigned tip and writes no root; opening one over a signed tip, a signed root or the signed chain.held a held open writes raises AuditLogAnchored.disclosure.record.unsigned-tipdisclosure.record.unsigned-tipA held open or signed check over a chain carrying no chain.held or signed root, whose tip is unsigned, raises AuditLogUnanchored; anchoring through the signing port signs that chain's missing roots and its tip.disclosure.erase.signer-bindingdisclosure.erase.signer-bindingErasure signs through the configured issuer or node's signing-port; readers verify the canonical audit chain under configured public-key pins, independently of transaction artifacts. Missing signing or verifier configuration follows recovery.
A read path holding no issuer key still appends, and a verifier needs no writer lock. Tamper evidence decided it: a signed chain.held marks a chain held for good.
| Option | Lost on | Cost |
|---|
| Held, unanchored or read-only, explicit anchoring (chosen) | — | An unanchored chain carries no signature until its owner anchors it. |
| A key for every append | Availability | A read path without issuer custody cannot serve. |
| Anchor silently at the first held open | Tamper evidence | A stripped, rewritten chain reads as unanchored and the next open signs it. |
append-group-holds — One append group holds the log at a time
Cited bydisclosure.record.read-entrydisclosure.record.read-entryEach 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, ping and tools/list append none.disclosure.record.single-writerdisclosure.record.single-writerOne append group holds a directory's audit log at a time: it takes audit.lock, links after the chain end, syncs, then releases the lock, so several processes append to one linear chain.disclosure.record.audit-keydisclosure.record.audit-keyExplicit 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 follow recovery; Unix creation excludes group and other permissions.disclosure.record.foreign-taildisclosure.record.foreign-tailAn 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 beyond group-commit.disclosure.record.read-onlydisclosure.record.read-onlyA read-only audit handle verifies the chain without the writer lock; an append through it raises AuditLogReadOnly.
serve --http and mcp run as separate processes on one project, and both append to its one linear chain.
| Option | Lost on | Cost |
|---|
| Directory lock per append group (chosen) | — | Groups across processes serialize at one sync at a time. |
| Directory lock for a process’s lifetime | Concurrent read processes | A second read process cannot start. |
| One chain per process | Truncation detectability | A deleted chain leaves no gap in any other. |
| A daemon owning the log | Scope | A third long-lived process and its transport to operate. |
audit-reads-view — audit_reads is a view over the chain, and a refused read appends
Cited bydisclosure.record.reads-viewdisclosure.record.reads-viewaudit query runs one statement over audit_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.disclosure.record.erasures-viewdisclosure.record.erasures-viewaudit query exposes audit_erasures from canonical erasure entries: transaction_id, subject_hash, affected_counts and executed_at; the projection retains no state and exposes no selector or row payload.disclosure.record.refused-readdisclosure.record.refused-readA read enforcement refuses appends one entry with outcome refused, naming the relations the guard parsed, and releases no rows.
One record decided it, and a refusal is typed and recorded (P2). A lookup over 24 h answers within 1 s.
| Option | Lost on | Cost |
|---|
| A view computed per query from the segments (chosen) | — | Query cost scales with the chain’s length; probes grow the chain. |
| A reserved table beside the chain | One record | A second state model drifts and needs its own erasure. |
| A projection rebuilt on append | One record | A crash between two writes leaves them disagreeing. |
| A refused read leaves no entry | Typed refusals | A probe of ungranted tables stays invisible. |
explanation-reads-exchange — An explanation reads the exchange policy and the chain, and keeps no store
Cited bydisclosure.explain.decisiondisclosure.explain.decisionaudit explain --table <t> --subject <principal> answers admit when a grant the exchange policy issues for the --role names, else its default_grants, carries read over a pattern covering the table, and deny otherwise.disclosure.explain.pathdisclosure.explain.pathA decision's path names the role or default set, the covering grant, then each step the table declares in relation-order; a deny's path ends at default-deny.disclosure.explain.replaydisclosure.explain.replay--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 recorded contextful. attributes.disclosure.explain.coveragedisclosure.explain.coverageAn 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.disclosure.explain.audiencedisclosure.explain.audienceaudit explain --audience <t> lists by name the roles whose grants read the table, whether default_grants do, and per on_behalf_of scheme the count of principals and served reads.
An explanation carries no row (P5) and qualifies every negative (P2).
| Option | Lost on | Cost |
|---|
| Current grants decide, the chain replays (chosen) | — | A decision states the grants in force at the call, not those a past read held. |
| A store snapshotting grants per read | One record | A second state drifts from the chain. |
| Replay the read itself to decide | No rows | Evaluating the relation touches rows. |
erasure-forced-rewrite — Erasure is a forced rewrite, a bounded cascade and a measured receipt
Cited bystore.declare.erasure-metadatastore.declare.erasure-metadataAn erasure declaration names, in keys of TableDecl, the table's erasure key, the table and column references citing it, and whether citing rows survive erasure.disclosure.erase.recoverydisclosure.erase.recoveryRecovery completes collection for a committed frontier and discards uncommitted replacements; an incomplete or disagreeing transaction record raises ErasureTransactionIncomplete before releasing store content.disclosure.erase.unsigned-recoverydisclosure.erase.unsigned-recoveryUncommitted 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.disclosure.erase.retirement-inventorydisclosure.erase.retirement-inventorySigned retirement inventories bind regular paths through domain-separated HMAC under audit-key and their file digests; recovery admits missing files as completed collection, validates all remaining files before deletion, and rejects changes through recovery.disclosure.erase.canonical-audit-keydisclosure.erase.canonical-audit-keyErasure and keyed retirement recovery require the persisted canonical audit-key; caller-supplied keys equal it, and absent or mismatched keys follow recovery before content access, signed intent, staging or collection.
One authenticated frontier spans every affected table, and readers and writers fence against it so an erased row never returns through an older batch. A failed audit publication leaves the original frontier selected.
| Option | Lost on | Cost |
|---|
| One authenticated publication record with release fencing (chosen) | — | Every reader and writer validates the frontier; collection rewrites retained artifacts. |
| Independent table pointer commits | Atomicity | A reader observes partial cross-table erasure. |
| Response-time tombstone filters alone | Physical removal | Retained files and sidecars preserve erased bytes. |
forced-rewrite-measured — Forced rewrite and measured receipt
Cited bydisclosure.erase.subject-columndisclosure.erase.subject-columnA table names its subject column as subject_id = "<column>" under [[pipeline.tables]]. A subject erasure against a table without one raises ErasureSubjectUndeclared, naming the table.disclosure.erase.key-setdisclosure.erase.key-setA key-set request binds its selectors to the declared keys of erasure-metadata and carries an opaque subject hash instead of a raw subject identity.disclosure.erase.surviving-citationsdisclosure.erase.surviving-citationsA citing table carrying the survival declaration of erasure-metadata retains its rows; its references resolve through erased-verdict.disclosure.erase.survivor-namesdisclosure.erase.survivor-namesErasure survivor snapshots use row-independent part names, preserve admitted surviving rows, record rewritten part key versions, and rebuild declared indexes through vector-by-fold and fulltext-by-fold.disclosure.erase.cascadedisclosure.erase.cascadeIn the same operation the cascade invalidates every derived fact whose provenance reaches the subject key or a tombstoned identifier within 16 hops.disclosure.erase.cascade-unboundeddisclosure.erase.cascade-unboundedA provenance chain deeper than the cascade bound raises ErasureCascadeUnbounded, and the erasure commits nothing.disclosure.erase.tenant-column-typedisclosure.erase.tenant-column-typeA tenant column of a non-string type raises PurgeTenantColumnType.disclosure.erase.tenant-undeclareddisclosure.erase.tenant-undeclaredA purge finding no model declaring an outermost partition key raises PurgeTenantUndeclared.disclosure.erase.owner-onlydisclosure.erase.owner-onlyA purge presented with a capability token raises PurgeRequiresOwner, evaluated before anything reveals store contents.disclosure.erase.tenant-rewritedisclosure.erase.tenant-rewritecontext erase --tenant <id> rewrites every row whose bare outermost partition column holds the identifier's exact bytes out of every table through atomic-publication, and prints a freshly signed receipt.disclosure.receipt.pseudonymdisclosure.receipt.pseudonymtenant_hash is SHA-256 over a random per-receipt salt and the tenant identifier. A receipt carrying the identifier in cleartext raises ReceiptIdentifierLeak before signing.disclosure.receipt.widened-claimdisclosure.receipt.widened-claimA coverage block naming a scope broader than its receipt_version declares raises ReceiptClaimWidened.disclosure.receipt.replayeddisclosure.receipt.replayedReturning the byte-identical earlier artifact in place of a freshly signed one raises ReceiptReplayed.disclosure.receipt.chain-recorddisclosure.receipt.chain-recordA purge appends its receipt's request_id and SHA-256 digest to the audit chain after signing; a digest the chain already holds follows replayed.
A purge deletes no bucket object, so the receipt claims rewrite-and-exclude over the canonical store. Replicas and the replication bucket sit outside the claim.
| Option | Lost on | Cost |
|---|
| Forced rewrite, bounded closure, measured receipt (chosen) | — | Cost scales with the table; fact reads go down until re-synthesis. |
| A one-hop cascade | Completeness | A fact derived from a derived fact outlives its evidence. |
| An unbounded transitive walk | Termination | Provenance is never proved acyclic. |
| Crypto-shredding a per-subject key | Fit with the substrate | Every write path holds a per-subject key forever. |
| A broad “data destroyed” receipt | Assertability | The claim is false when signed. |
Revisit: a purge deletes bucket objects; 72 h request-to-last-replica erasure becomes an engine bound.
re-synthesis-after — Re-synthesis after erasure replays the extraction cache
Cited bydisclosure.erase.atomic-publicationdisclosure.erase.atomic-publicationSubject and key-set erasure select one committed frontier across every affected table; a failed transaction exposes none of its staged replacements.disclosure.erase.reader-frontierdisclosure.erase.reader-frontierQuery, file, retrieval and cache responses resolve one erasure frontier and revalidate it before releasing bytes; an obsolete frontier releases no rows or files.disclosure.erase.shared-referencesdisclosure.erase.shared-referencesA shared digest survives while any surviving declared reference names it; erasure collects a digest only after its last surviving reference disappears.disclosure.erase.subject-pseudonymdisclosure.erase.subject-pseudonymSubject 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.disclosure.erase.row-free-auditdisclosure.erase.row-free-auditThe signed audit chain binds the committed transaction, subject-pseudonym and affected counts to the exact replacement-file inventory.disclosure.erase.store-bindingdisclosure.erase.store-bindingThe signed erasure entry binds store-identity; readers reject a frontier whose signed store identity differs from the receiving store, following recovery.disclosure.erase.working-publicationdisclosure.erase.working-publicationErasure 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.disclosure.erase.recovery-verbdisclosure.erase.recovery-verbcontext erase --recover --project <project> resumes recovery under privilege and effect-boundary before collection; selectors and signing ports are absent.disclosure.erase.physical-removaldisclosure.erase.physical-removalWithin 24 h of the erasure's commit, every file holding an erased row, superseded snapshots inside retention included, is rewritten or collected.
An erased row reaches derived state through its claims, its batch’s extraction-cache entry and any dead-letter row. A batch with no erased row replays without a model call only at its original boundaries under an unchanged template, model and cutoffs.
| Option | Lost on | Cost |
|---|
| Collect affected entries; replay untouched batches (chosen) | — | Replay needs each batch’s recorded membership, else restaging re-extracts. |
| Flush the whole cache on any erasure | Cost | Every restaging re-extracts the full table. |
| Leave entries to their own retention | Physical removal | A response quoting the erased row outlives the deadline. |
| Restage under the erasing credential | Attribution | Restored claims carry the forget grant. |
drive-permission-coverage — Drive permission coverage includes every selected file
Cited bydisclosure.drive-sweep.permission-pagesdisclosure.drive-sweep.permission-pagesFor each governed file, the sweep follows every permissions.list page under the connection credential before its grants replace prior grants.disclosure.drive-sweep.unreadable-resourcedisclosure.drive-sweep.unreadable-resourceA refused or incomplete permission read marks its resource unread and advances no coverage watermark under coverage-watermark.
A refused permission page does not prove that a file’s prior grants remain complete.
| Option | Lost on | Cost |
|---|
| Complete per-file permission reads (chosen) | — | A refused file stalls coverage and can close reads past the budget. |
| Treat a refused file as grantless | Truthfulness | Empty grants conceal an unread resource. |
| Advance after the first permission page | Revocation coverage | Later pages can hold the revoked principal. |