contract disclosure · 60-visibility.md · 61-disclosure.md · 62-accountability.md · guide · card
Aggregate disclosure
A derived result answers a question over many contributors without handing any contributor’s rows to the party asking. This file fixes the setting a result is derived in, the ordered release, the suppression signal a consumer sees, the reviewed templates a releasing principal executes, and the line between a per-person table and a cohort table.
A derived result, from the deployment’s setting to the published rows:
set-mode
The two deployment settings a derived result is computed in, the offline diagnostic, and the cross-owner hashed join.
check-verbtestcontextful disclosure check --config <path>diagnoses each published model’s disclosure policy and statement from local files and reports every refusal by model name.offline-diagnostictestThe single-operator diagnostic reads the manifest and each published model’s statement text from local disk and issues 0 requests to the object store.aggregate-shapetestA statement is aggregate-shaped when its parsed SELECT groups rows, has DISTINCT or HAVING, or calls an aggregate catalog function outside a window expression.policy-absenttestA published model matchingaggregate-shapeand carrying neither a disclosure policy nor a recorded opt-out fails the diagnostic withDisclosurePolicyAbsent.opt-out-recordtestA model records an opt-out as a nonemptydisclosure_opt_outreason; combining it with[model.disclosure]raisesspec-invalid.model-unreadabletestA published model whosesql_fileunderstatement-sourcedoes not read raisesDisclosureModelUnreadable.clean-room-preconditionstestA 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, raisesDisclosureCleanRoomPreconditionUnmet.hashed-jointestTwo 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 raisesDisclosureStaticPepper.pepper-versiontestA cross-owner join accepts only rows carrying its pair’s current pepper version; rotation re-lands both owners’ join keys before the next release.
unsettled: Which CLI or MCP verb opens a cross-owner store and runs hashed-join and pepper-version, given that no production path reaches open_cross_owner? owner: disclosure affects: disclosure.set-mode
release
The ordered statistics release, the write-time disclosure policy, and the per-unit budget reservation.
model-policytestA publishedmodel-blockgoverned by aggregate disclosure declares[model.disclosure];metric_boundsnames each noised sum’s lower bound, upper bound and quantum.release-verbtestcontextful disclosure release <model> --project <project>builds the named model from its declared statement, applies its disclosure policy, and publishes one derived table.governed-buildtestA build of a model declaring[model.disclosure]outsiderelease-verbraisesDisclosureReleaseRequiredand publishes nothing.Why
because a plain build publishes the statement’s per-contributor rows, contributor key included; only the release step replaces them with a derived tablebudget-reservationtestBefore computing, a release reserves each contributing unit’s declared per-run spend against its lifetime cap in one catalog transaction. A failed reservation raisesDisclosureUnitBudgetExhausted, and the release publishes nothing.noise-mechanismtestA 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.metric-boundstestA 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 raiseDisclosureMetricBoundsAbsent.release-envelopetestA release reports its published table, policy hash, reserved per-run spend, suppressed-group presence and row count without carrying a contributor key.lookalike-refusaltestA release requesting an activation handle or lookalike segment, as--output activation-handleor--output lookalike-segment, raisesDisclosureLookalikeUnsupportedand publishes nothing.spend-declarationtest[model.disclosure]declaresper_run_spend, the positive epsilon one release reserves from each contributing unit; a release whose policy declares none raisesDisclosureSpendUndeclaredand publishes nothing.lifetime-captestA project manifest’s top-level[disclosure]block declaresunit_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.budget-ledgertestA 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.duplicate-graintestA non-numeric, non-grain column rides the grouping, and one not constant within its group raisesDisclosureDuplicateGrain.group-keytestA group key not a subset of the permitted grouping columns raisesDisclosureGroupKeyNotPermitted.contributor-keytestA governed statement omitting the contributor key raisesDisclosureContributorKeyAbsent.forbidden-columntestA contract declaring a forbidden column, or a materialization producing one, raisesDisclosureForbiddenColumnPublished. The check runs after the contributor key leaves the published columns.
suppress
The group-size floor, the contributor-share ceiling, and the single signal a withheld group leaves.
min-group-sizetestmin_group_sizecounts 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 raisesDisclosureMinGroupSizeBelowFloor.dominance-unverifiabletestA group under a share constraint whose per-contributor masses are unavailable raisesDisclosureDominanceUnverifiableand is suppressed.empty-policytestA policy setting neither threshold, or only a share ceiling at a whole group’s mass, raisesDisclosurePolicySuppressesNothing.grouping-allowlisttestAn empty permitted-grouping list, or a permitted name that is not column-shaped, raisesDisclosureGroupingAllowlistEmpty.sentinel-rowtestA release withemit_sentinel = truerepresents every suppressed group together as one__suppressed__row carrying no group key or exact figure.
template
Query templates a releasing principal calls by name: the single-statement rule, the checks they pass, and the truncation flag a capped read carries.
single-statementtestA declaration holding more than one statement raisesDisclosureTemplateMultiStatement.overfetchtestA capped result sets the envelope’s truncation flag by reading exactly 1 rows past the effective ceiling.read-envelopeA template read returns rows, truncation, row ceiling, suppression presence and the disclosure policy hash; set-valued policy fields followdisclosure-digestat every nesting depth.
unsettled: Does a template read take read-envelope’s suppression presence and policy hash from the release envelope its table was published under, and which surface serves that read? owner: disclosure affects: disclosure.template
bound-cohort
The line between a per-person table and a cohort table, and the floor a cohort read carries.
governance-declarationtest[model.disclosure]declaresgovernance,per-personorcohort, naming whether each published figure describes one person or a cohort; an absentgovernanceisper-person, and another value raisesspec-invalid.Why
because an undeclared model takes the stricter regime: a per-person model refuses exact figures, so omittinggovernancenever widens what a release publishescohort-wideningtesttestA template or statement read grouping acohortmodel’s published table by a strict subset of the group key its release records raisesDisclosureCohortWidening; grouping by the whole key or a superset of it reads.Why
because a coarser grouping sums released groups into figures no release noised or floored, and the recorded key decides each read without a ledger of earlier releaseswidening-groupingstestA SELECT groups each cohort table itsFROMreaches by eachGROUP BYset, each aggregate window’s partition list, and the empty set when it aggregates under neither; only a bare column reference covers a key column.Why
because an expression over a key column can merge its values, so only the column itself keeps released groups apartper-individual-rowtesttesttesttestA query, template, retrieve, row, subscribe or recall read touching a per-individual table raisesDisclosurePerIndividualUngrantedunless a grant holdsread-individualover that table; a derive task’s input read is a row read.Why
because the face applies no floor to an ad-hoc read, so a read of a source table returns contributors’ rows that no release has suppressedderived-individualtesttesttesttesttestA table acohortmodel’s statement names is per-individual, and so is each output table of a derive pipeline whose source or joined table is per-individual, and each claims and dead-letter table of asynthesizejob reading one, transitively.Why
because a derive or synthesis pass carries its parents’ rows, or claims extracted from them, into its output, which then holds contributors’ rows no release has suppressedderive-individualtestA derive run whose source or joined table is per-individual refuses asper-individual-rowat run start, before selecting any unit, unless its credential holdsread-individualover that table; a run without a credential refuses too.Why
because the run’s credential is the only authority a derive holds, and refusing before selection lands no rowsynthesize-individualtesttestA synthesis pass whose source or target is per-individual refuses asper-individual-rowat run start, before reading a row or calling its endpoint, unless its credential holdsread-individualover that table.Why
because a pass reads its target’s live claims as well as its source, and refusing before the first batch spends no model callsingleton-cohorttesttestAcohortmodel whose permitted grouping columns or non-contributor grain columns name its contributor key or a forbidden column raisesDisclosureSingletonCohortat declaration, before any row lands under it.exact-figurestesttestA policy declaringfigures = "exact"on aper-persontable raisesDisclosureExactPerPerson.figuresdefaults to"noised", and exact figures publish only from acohorttable.
Shapes
A disclosure policy on a governed model:
[model.disclosure]
grouping_allowlist = ["category", "region", "period"]
contributor_key = "tenant_id"
min_group_size = 5
max_contributor_share = 0.4
emit_sentinel = true
figures = "noised" # the default; "exact" only on a cohort table
governance = "per-person" # the default; "cohort" for figures describing a cohort
forbidden_columns = ["tenant_id", "subject_id", "account_email"]
per_run_spend = 0.5 # epsilon reserved from each contributing unit per release
[model.disclosure.metric_bounds]
value = { lower = 0, upper = 1000000, quantum = 1 }
[disclosure]
unit_lifetime_cap = 10 # epsilon each contributing unit spends across every release
The breakdown a governed statement emits, and the rows published from it:
-- emitted: one row per (group, contributor)
category region period tenant_id value
group-a emea 2 t-0191 142000
group-a emea 2 t-0044 128000
group-a emea 2 t-0332 111000
group-a emea 2 t-0072 102000
group-a emea 2 t-0088 88000
group-b emea 2 t-0044 55000
-- published after noisy partition selection, suppression and roll-up;
-- the figure is noised, not the exact sum 571000
category region period value
group-a emea 2 568940
__suppressed__
The envelope a capped template read returns:
{
"rows": [{ "period": 2, "value": 568940 }],
"truncated": true,
"row_ceiling": 500,
"suppression_present": true,
"disclosure_policy_hash": "sha256:4f1c…"
}