ADR-028: Determination Input Snapshot

On this page

Status

Accepted (2026-06-02)

Amends

ADRs are immutable once accepted, so this ADR amends ADR-002 rather than editing it. Read both together: ADR-002 makes a determination a signed, minimal black-box output; this ADR requires that the signed determination also bind the inputs it was computed from, while preserving ADR-002’s guarantee that the orchestrator never sees the data behind a determination.

Delivery tracks

  • v1 (Track 1, SNAP-UAT-minimum): a flat input snapshot — the proven facts the SNAP determination read, each with its provenance, plus the resolved policy parameters and the ruleset corpus content-hash in force — immutable and bound to the program service’s signature. This satisfies appeals (adjudicate on the facts as they stood) and QC (reproduce the inputs) and records which ruleset version produced the verdict.

  • v2 (Track 2, post-UAT correctness): the self-explaining fact graph — derived facts with their derivation edges and per-rule versions — plus cross-program input capture, the FTI-bearing program snapshots, and their entry into the ADR-014 chain.

Context

ADR-002 makes each program service a black box returning a signed, minimal determination. The orchestrator assembles an ApplicationContext (crates/canopy-contracts-eligibility/src/determine.rs:88, built at services/canopy-eligibility/src/orchestrator.rs:658-675), POSTs it to each program’s /v1/determine, verifies the returned JWS, and persists a ProgramDetermination. The problem: nothing snapshots the inputs.

  • snap_determinations (services/canopy-snap/migrations/20260326000000_create_snap_tables.sql:13-29) stores 15 columns — all verdict/output fields plus the JWS signature and program_service_version. No column holds the income/asset/expense/household facts evaluated. The signed bytes cover only the determination’s own wire format (services/canopy-snap/src/store/mod.rs:45-49, #338).

  • The orchestrator’s ApplicationContext is assembled fresh from live canopy-persons reads and then discarded.

  • The income editor’s code comment — "determinations carry their own income snapshot in the signed JWS" (services/canopy-web/src/api/income.rs:6-11) — is false: the Determination struct (services/canopy-eligibility/src/determination.rs:9-28) carries outcomes, not inputs.

  • Several partial, inconsistent mechanisms already exist and prove the need: tanf_household_snapshots (services/canopy-tanf/migrations/20260325000000_create_tanf_tables.sql:19-33) — a typed but narrow snapshot of household-shape aggregates only, snapshot_at but no DB immutability; snap_applications.application_context (…​:8) — an untyped, freely UPDATE-able JSONB blob; and a separate tanf_income copy keyed by application. None captures the full input picture in one place, and none is enforced immutable.

This was survivable while facts were a single mutable row. Once facts become valid-time-versioned and correctable (ADR-027), "what did this determination see?" becomes unanswerable from the live store — breaking appeals (a hearing must adjudicate on the facts as they stood) and QC / Pub 1075 (reconstructable inputs). The determination must carry its own inputs.

Decision

At the moment it renders a verdict, each program service freezes the inputs it evaluated into a read-only artifact bound to the signed determination — the determination input snapshot.

What the v1 snapshot contains (three input classes)

A determination’s inputs are not only persons facts. The v1 snapshot captures, as a flat, typed record:

  1. Proven facts with provenance — every input fact the program read (each person + income/assets/expenses + household composition as of the effective date), each captured as a value plus its provenance: source, verification status, the author of record, and the fact version identity (the composed assistance-unit picture). For an IEVS-accepted fact the provenance also carries the original proposed value + proposing source (ADR-027 §2), so the leaf is self-contained.

  2. Resolved policy parameters — the actual threshold values used (the jurisdiction.toml values injected at startup — FPL/SUA/elderly-age etc.) and the federal-parameter table version, so the verdict is reproducible after a later policy update.

  3. Ruleset corpus version — the content-hash of the deployed JDM corpus that produced the verdict (a startup-computed SHA-256 of the loaded ruleset set, surfaced by canopy-rules on each evaluation — a small canopy-rules prerequisite).

v1 does not include the per-fact derivation graph (which derived fact came from which inputs via which rule). That self-explaining-graph fidelity, plus capture of cross-program inputs (EE15 assigned_coa, the event-propagated ELE state, TMA — captured by value plus the upstream determination id) and SOLQ/FDSH records, is v2 / Track 2. v1 explicitly scopes its reproducibility claim to single-program, persons-plus-policy verdicts; cross-program reproducibility is a named Track 2 deliverable, not an asserted v1 property.

NOTE
Cross-program capture is partially realized as of T2-3 (#684) — see Amendment 1 — Cross-program input capture (SOLQ) realized (T2-3, 2026-06-21). The SSA SOLQ record is now frozen by value in the snapshot; the derivation graph and the remaining cross-program inputs stay v2/Track-2.

Where it is assembled and signed (preserves ADR-002)

The snapshot is assembled and signed inside the program service, not the orchestrator. The /v1/determine request is enriched so the context the orchestrator sends already carries per-fact provenance (and the resolved policy params + ruleset corpus-hash). The program service builds the snapshot from what it received plus its own evaluation, persists it immutably in its own database (ADR-001), and the orchestrator receives only outcome + a snapshot content-hash — never the snapshot cleartext. This preserves ADR-002 (the orchestrator stays a black-box consumer and, for FTI programs in Track 2, never receives the FTI-bearing graph) and resolves the "signer is not the holder of the fact graph" problem (the program service is now both).

Binding, immutability, and storage

  • Binding (decided): the snapshot is serialized to canonical bytes (RFC 8785), hashed (SHA-256), and that hash is a signed field in the determination’s canonical_signing_payload (crates/canopy-signing); the snapshot blob is stored alongside the determination. This is the only defensible option — signing an arbitrarily-large nested graph via the legacy whole-struct serde_json::to_vec path (services/canopy-eligibility/src/determination.rs:46-58) is non-canonical and is forbidden for anything carrying the snapshot; that divergent signer is reconciled/retired before this lands. A snapshot_hash: Option<String> field is added to the shared SignableDetermination (a coordinated canopy-signing + 5-service + verifier change, deploy-ordered verifier-tolerant-first — not five independent per-service changes; the existing program_extension field is already in use and is not repurposed).

  • Immutability: append-only, never updated, DB-enforced where feasible (no UPDATE path; forward-only per ADR-016).

  • Storage / FTI (track split): for SNAP (non-FTI) the v1 snapshot is bound to the JWS and forward-only-immutable. For canopy-tanf / canopy-medicaid the snapshot carries FTI-derived facts and stays within those services' Pub 1075 boundary (ADR-004); making those FTI-bearing snapshots join the ADR-014 hash chain (so FTI-at-rest gets the §4 tamper-evidence + §9 breach pathway every other FTI artifact has) is Track 2 work, scoped with the program fan-out.

  • Key retention: the snapshot embeds the signing kid; verification keys are retained as long as any determination they signed could be appealed or QC’d, independent of the operational JWKS rotation window — otherwise a long-delayed appeal finds the signature unverifiable.

Supersession and legacy determinations

  • Supersession: a new determination (an adjustment re-determination or a recert) records the previous_determination_id it supersedes and the effective period it governs. Prior determinations + their snapshots remain immutable and queryable, flagged superseded-as-of. The materiality check (ADR-027 §6) diffs against the operative (latest non-superseded for the date) snapshot; overpayment recalc walks the chain. A program-service read endpoint exposes a determination’s frozen snapshot (or its comparable verdict/amount + versions) for the cross-service materiality/overpayment callers.

  • Legacy determinations: determinations predating this ADR have no input snapshot. They carried an explicit no_input_snapshot marker; appeals/QC for them fell back to the (explicitly weaker) reconstruction-from-audit backstop. This gap was named and bounded, not silent — and was RETIRED by #911 (per ADR-036’s contraction): snapshot_hash is now required (NOT NULL, five services), the pre-snapshot legacy rows were deleted (devstack re-seeds; no production data existed), and the snapshot_status marker + NoInputSnapshot wire vocabulary were removed. The partial mechanisms (tanf_household_snapshots, snap_applications.application_context, the tanf_income copy) are superseded via expand-contract (ADR-016) — retained for already-signed determinations they participated in, not dropped.

Effect on the audit and history model

This snapshot, with ADR-027’s valid-time versioning, moots canopy-security as a load-bearing correctness read. Appeals replay from the snapshot; overpayment recalc compares the snapshot to a dry-run re-determination on the corrected (versioned) facts at the recorded ruleset version. canopy-security stays a pure, append-only audit ledger — useful for "who changed what when," not required to reconstruct any single determination. Reconstruction-from-audit is retained as a backstop, not a dependency.

Consequences

  • Each program service persists an immutable, signature-bound input snapshot in the same transaction as the determination. canopy-contracts-eligibility gains the snapshot type; SignableDetermination gains snapshot_hash. The orchestrator enriches the /v1/determine context with provenance and receives only the hash.

  • canopy-rules gains a corpus content-hash surfaced per evaluation (the v1 prerequisite).

  • Appeals (canopy-appeals) and overpayment recovery consume the snapshot inside the owning program service for FTI programs (a hearing-scoped in-boundary read), never by pulling FTI into canopy-appeals/canopy-reporting — those services have no Pub 1075 controls (ADR-004). This consumer work, the FTI-bearing snapshots, the ADR-014 chain extension, and the derivation graph are Track 2.

  • Storage grows by one input snapshot per determination; bounded by determination volume, and the price of a reconstructable, appeal-defensible determination.

Alternatives considered

Alternative 1: Reconstruct inputs from the canopy-security audit trail. Rejected as primary — fragile, slow, and turns the audit ledger into a correctness dependency. Kept only as a backstop (and the sole option for legacy determinations).

Alternative 2: Re-read live facts at appeal time. Rejected — once facts are versioned/correctable, the live store no longer reflects what the determination evaluated.

Alternative 3: Snapshot a flat copy of inputs with no provenance. Rejected — cannot distinguish a firm fact from an IEVS lead or show why a conclusion was reached. (Note v1 is flat but carries provenance + policy params + ruleset version; the rejected option is the provenance-less copy.)

Alternative 4: Sign the full snapshot as a struct field the orchestrator verifies. Rejected — for FTI programs the orchestrator would then receive the FTI-bearing cleartext (the ADR-002 / ADR-004 breach). The program signs a snapshot hash; the orchestrator verifies outcome + hash without the cleartext.

Amendment 1 — Cross-program input capture (SOLQ) realized (T2-3, 2026-06-21)

Status unchanged (still Accepted); this records what the T2-3 (#684) slice of the cross-program-input class (above) actually realized, vs what stays deferred. ADRs are immutable once accepted, so this is an in-document amendment, not an edit to the Decision.

  • Realized — SOLQ by value. canopy-medicaid now freezes a by-value projection of the SSA SolqRecord (all fields + person_id) into a new typed DeterminationSnapshot.cross_program_inputs.solq field, so an ABD verdict reproduces even if the SOLQ→ABD-flag derivation later changes. The previously captured value was only the five derived ABD booleans (in program_input.member_flags); those stay (the resolved ruleset inputs), with the raw projection now alongside as the source. A snapshot carrying cross_program_inputs is schema_version: 2 (a re-verifier must honor the version — an unknown version must be refused, not silently re-hashed); a snapshot without it stays schema_version: 1, byte-identical to a pre-T2-3 snapshot. The FTI-bearing snapshot’s ADR-014 chain entry records solq in data_elements_accessed (field name only, per ADR-014 §2) when SOLQ was frozen.

  • Already satisfied (no new capture needed). EE15 assigned_coa is the Medicaid determination’s own output, already captured in program_input; ELE produces no snapshot and its cross-program linkage already lives in ele_grant_events.source_determination_id/application_id; the TMA inputs (tanf_termination_date, had_tanf_in_prior_months) are already frozen in program_input.

  • Still deferred (Track 2 follow-ups). The TMA upstream determination id by-reference (needs a tanf.case_closed event-contract change — no TANF determination id reaches the Medicaid TMA flow today) and FDSH capture (FDSH is not yet consumed by determine() — there is nothing to freeze until the gating feature exists). The per-fact derivation graph remains v2.

Amendment 2 — Self-explaining derivation graph realized (T2-2, 2026-06-23)

Status unchanged (still Accepted). This records what T2-2 (#679) realized of the "v2 / Track 2" self-explaining derivation graph deferred by the Decision (the "which derived fact came from which inputs via which rule" graph), and the decisions taken to bound it. In-document amendment, not an edit to the Decision.

  • Realized — the typed derivation graph, inline on the snapshot. A new typed DeterminationSnapshot.derivation_graph: Option<DerivationGraph> (in the shared canopy-contracts-eligibility crate) captures, at determination time, every derived fact’s value plus the edges that produced it: DerivationEdge { inputs: Vec<FactPath>, outputs: Vec<FactPath>, source: EdgeSource } and DerivedFactNode { path, value, is_provisional, provisional_reason }. A FactPath addresses the frozen snapshot by typed coordinate — Leaf (a facts position, with the optional corpus fact_id), Input (a program_input path, for programs with no itemised leaves), Param (a policy_params key), CrossProgram (a cross_program_inputs path — e.g. a SOLQ leaf), or Derived (another node in this graph). The graph rides the existing canonical_bytes()snapshot_hash → signature chain and the append-only trigger — no second hash surface, no join at re-verify.

  • schema_version = 3 iff a non-empty graph; single empty encoding. A snapshot carrying a non-empty derivation_graph is schema_version: 3; the skip_serializing_if predicate omits the field for both None and Some(empty), so both canonicalize byte-identically and an empty graph never rotates a hash (the same discipline as Amendment 1’s v2). verify_schema_version refuses an unknown (> 3) version on re-verify rather than silently re-hashing.

  • rule_version IS the corpus_hash (no per-rule semver). The only on-disk rule version is corpus-level. A rule and the policy-param table it reads ship together and must stay mutually consistent; a per-rule semantic version that could drift from its param version would be a false guarantee. So the issue’s "per-rule versioning" is realized as per-rule traceability (RuleRef: ruleset_name + node_id + node_kind + winning rule_id_in_node) at corpus version granularity (corpus_hash). Tradeoff (stated, not hidden): any single-rule change rotates the whole corpus_hash, so the hash alone cannot attribute which rule changed — per-rule attribution comes from corpus diffs
    the JDM files' git history, not the hash. corpus_hash versions JDM firings only (EdgeSource::Jdm); Rust-side derivations carry EdgeSource::RustFn { fn_name, service_version } instead (the determination’s program_service_version).

  • #669 provisional derived nodes. An inferred (not worker-verified) derived fact is frozen as DerivedFactNode { is_provisional: true, provisional_reason }: the SNAP inferred utility tier (infer_utility_tier, from expense leaves) and the TANF inferred deprivation basis (infer_tanf_deprivation, from member facts). This is the substrate T2-8 uses to exclude provisional-derived chains from automated recovery. Medicaid SOLQ-absent ABD flags default to false — a default, not an inference — so they are is_provisional: false (#669 is not extended to them).

  • Per-program capture (all five program services). SNAP (reference impl, MR4), TANF (MR5), Medicaid (per-subject, ADR-035, MR6), CAPS + WIC (per-subject, MR7) each capture both their JDM-internal firings (the engine folds its per-node trace into typed RuleFiring`s; the program service rewrites them into snapshot `FactPath coordinates) and their Rust-side derivations. The Medicaid ABD chain references SOLQ by reference (FactPath::CrossProgram) — no FTI/SSA value is copied into an edge (ADR-014; see the ADR-014 note).

  • Granularity boundary (no information lost). zen-engine’s trace gives field-level inputs for decision-table nodes (reference_map) but only the node-level input object for expression nodes; the bare input/context passThrough envelope roots are dropped (they are namespace wrappers, not derivation inputs — #903). The exact field references an expr- node reads live *authoritatively in the corpus_hash-pinned ruleset; re-encoding them into the snapshot would duplicate and risk drifting from that source of truth, so node-granular expr- inputs is the architecturally-honest unit. Expression-node *outputs are field-level and ARE captured.

  • Still deferred (Track 2 follow-ups, filed + /relate #679). The Medicaid TMA upstream-determination by-reference edge (blocked on the tanf.case_closed det_id contract change) and FDSH-input edges (blocked on FDSH being consumed by determine()) — both literally blocked by unshipped work. The Medicaid denial-reasons (per-COA) + cmd-cascade-priority edges are captured in program_input rather than re-expressed as graph edges (#904). Field-path-precise expression-node input edges, a rule→regulation citation (an ADR-011 capability), a denormalized per-rule reporting query column, and the full DeterminationSnapshot ToSchema sweep (so OpenAPI documents derivation_graph) are non-blocked, measured-need follow-ups.

Amendment 3 — Snapshot replay inputs realized (dry-run materiality) (T2-7, 2026-06-25)

Status unchanged (still Accepted). This records what T2-7 (#680) realizes of the "dry-run re-determination …​ at the recorded ruleset version" the Decision named (the materiality/overpayment mechanism), and the snapshot-completeness decision it forced. In-document amendment, not an edit to the Decision.

  • The snapshot’s "Resolved policy parameters" is completed to the full verdict-affecting bundle. The Decision requires the snapshot freeze "the actual threshold values used …​ so the verdict is reproducible after a later policy update." T1-10’s implementation captured only the 14 main SNAP eligibility thresholds (build_snap_eligibility_thresholds) — but the pay-period conversion factors and the self-employment-deduction settings (pct + enabled) also drive the verdict (applied before the main ruleset). T2-7 enriches policy_params to the full bundle, so a dry-run re-determination is a faithful replay. This is the complete verdict-affecting set (certification_months/renewal_months set dates, not the verdict, and are excluded). A snapshot without the bundle keys is a pre-T2-7 snapshot.

  • The recorded ruleset corpus version is now replayable, not merely recorded. v1 recorded the corpus_hash but canopy-rules could only evaluate against its single live corpus. T2-7 adds ruleset-version history to canopy-rules (a ruleset_corpus_versions store keyed by corpus_hash, populated at startup) + a corpus-pinned /v1/evaluate, so the dry-run replays the exact corpus the baseline recorded. Forward-looking every new determination’s corpus is persisted; pre-T2-7 corpora are not replayable (the bounded backfill boundary).

  • Dry-run is non-persisting + write-free + unsigned. The materiality/overpayment dry-run (§Supersession / §Effect on the audit and history model) is realized as a dedicated non-persisting path that writes no determination/snapshot AND, via an ephemeral ?audit=false rules mode, no rules-audit rows; its output is unsigned (it is not a determination of record).

  • Replay degradation is typed, never wrong. A baseline with no snapshot, an incomplete (pre-T2-7) policy bundle, or an unreplayable corpus returns a typed NoBaselineSnapshot / CorpusUnavailable to the caller (renewals → manual review), never a verdict computed against the wrong policy.

Amendment 4 — In-boundary overpayment recompute + hearing-view realized (T2-8, 2026-06-27)

Status unchanged (still Accepted). This records what T2-8 (#681) realizes of the §Consequences §70 statement that "appeals and overpayment recovery consume the snapshot inside the owning program service for FTI programs (a hearing-scoped in-boundary read), never by pulling FTI into canopy-appeals/canopy-reporting." In-document amendment, not an edit to the Decision. SNAP-only (the only non-FTI, replay-capable program); the architecture is program-generic so the FTI programs adopt it unchanged once they have replay paths.

  • The consumer work is realized in-boundary. When a worker authors a retroactive fact correction on a past SNAP determination, the owning program service (canopy-snap) recomputes the correct verdict by replaying the determination’s frozen snapshot (T2-7 Amendment 3’s replay machinery) against the corrected facts, sizes a 7 CFR 273.18 overpayment claim (#382 store), and emits snap.overpayment_claimed → a new OverpaymentNotice. The recompute orchestration (snapshot read → context assembly from canopy-persons → replay → claim → notice) runs entirely within canopy-snap — which gains its own persons + enrollment clients — so for an FTI program no restricted fact ever leaves the service (the orchestrator never assembles the FTI context). The hearing-view + the notice/claim payloads carry IDs + the verdict/amount + a non-FTI summary only.

  • A hearing-scoped, FTI-safe read is added. GET /v1/determinations/{id}/hearing-view returns a non-restricted, unsealed projection of a frozen determination (verdict, benefit, non-FTI fact summary, version identities) for appeals to display at a hearing — distinct from the sealed, service/admin-gated /snapshot. FTI-safety comes from the projection (no sealed ciphertext / restricted leaf in the DTO), shaped so an FTI program returns an FTI-redacted projection.

  • Replay reads facts as-of the baseline as_of, corrected. The recompute reads the household as-of the snapshot’s recorded evaluation date (as_of) — at which the retroactive correction’s valid_from already applies — not the correction’s effective date; the correction date bounds only the overpayment window (capped at the determination’s supersession so a later determination’s months are never clawed back). This is the backward-looking complement to T2-7 Decision J, which deferred retroactive corrections from the forward-looking materiality nudge.

  • Provisional-derived determinations are excluded from automated recovery. If the snapshot’s derivation graph (Amendment 2) holds any is_provisional node, the recompute returns a typed ProvisionalExcluded (manual review) — never an automated claim off a worker-unverified inferred input. The flat derivation graph has no verdict node, so the gate is the conservative "any provisional node present" (can only over-exclude, never under-exclude).

  • The two overpayment paths stay separate + overlap-guarded. The new determination-error path (snap.overpayment_claimed) is distinct from the existing continued-benefits recoupment path (appeal.overpayment_assessed); a per-household-locked guard prevents the recompute from double-claiming a determination an open/in-repayment claim already covers.

Amendment 5 — Explicit policy-parameter version stamp realized (D4, 2026-06-30)

Status unchanged (still Accepted). This realizes the part of §39 the original Decision named but T1-10 left unimplemented — "the federal-parameter table version, so the verdict is reproducible after a later policy update" — and records the schema_version consequence. In-document amendment, not an edit to the Decision. Program-generic (all five programs), shipped via #880 + #906.

  • The "…​table version" is now captured as a required field. T1-10 froze the resolved policy_params values + the ruleset corpus_hash, but no explicit parameter-table version. D4 adds the required policy_params_version field, sourced from a new required [meta].version key in each jurisdiction’s rulesets/<slug>/jurisdiction.toml, loaded once via the shared canopy_common::settings::load_jurisdiction_policy_version so all five programs stamp it identically. Where policy_params records the resolved values, this is the operator-maintained revision label of the whole parameter set (the federal tables under rulesets/federal/ plus the jurisdiction’s own thresholds/options) — so a re-verifier / QC reviewer knows exactly which revision produced a verdict even when the resolved values would round-trip identically. Required (no skip_serializing_if), no silent default: a missing/empty [meta].version is a fatal startup configuration error.

  • schema_version is bumped to 5, and the floor moves to 5. Every snapshot now carries the version stamp, so the accepted window is exactly [5, 5]; verify_schema_version refuses anything below 5 (v1–v3 a plaintext downgrade; v4 the sealed-but-pre-version-stamp format) and anything above MAX. Pre-1.0 there are no stored snapshots crossing this boundary (devstack re-seeds) and no previously-signed hashes to preserve, so v4 is dropped outright rather than migrated — a v4 blob also fails to deserialize, since the new field has no serde default. The below-floor refusal variant was generalized from PlaintextSchemaVersionRejected to SchemaVersionBelowFloor to name both cases accurately.

  • The snapshot wire shape is now self-documenting (OpenAPI). The whole DeterminationSnapshot type tree (incl. the Amendment 2 derivation_graph and the new policy_params_version) derives utoipa::ToSchema (#906), so GET /v1/determinations/{id}/snapshot declares the typed body instead of an opaque Object; the wire shape previously documented only in prose now appears in docs/modules/ROOT/openapi/snap.json.

See the Backlog Cleanup Campaign plan (Phase D / D4). Related issues: #880, #906, #678.

Amendment 6 — Composite policy target: as_of-faithful parameters attested (#1467, 2026-08-14)

Status unchanged (still Accepted). Realizes ADR-002 Amendment 1 D8’s "policy pinned per run" for the parameter half: a ruleset corpus hash alone cannot witness an annual COLA (the rules loader deliberately skips parameter JSONs), so nothing signed content-addressed the money tables that scored a case. SNAP-only today; the shape is program-generic.

  • The composite policy target. canopy_common::policy_target::PolicyTarget = {corpus_hash, params_digest, effective_period}. The digest newtypes (CorpusHashHex, ParamsDigest) validate 64-lowercase-hex through construction AND deserialization (try_from), so malformed provenance — including an older engine’s omitted-hash "" — is unrepresentable, never signable. EffectivePeriod is half-open [start, end_exclusive) with both bounds definite: a set’s validity is intrinsic (for the snap-cola family, end_exclusive = the first October 1 strictly after start), never derived from what other files are staged — the same bytes always produce the same target, and an expired set fails selection closed (CANOPY_SNAP__ALLOW_EXPIRED_PARAM_SET is the accountable override, default false, error-logged per use).

  • params_digest scope + encoding. SHA-256 over, in name-sorted order, len(name) as u64 LE || name || sha256(raw file bytes) for the COMPLETE policy-parameter input set: the selected effective-dated snap-{allotments,deductions,income-limits}-.json triple, snap-budgeting-factors.json, and the jurisdiction’s jurisdiction.toml (name convention, KAT-pinned: the four federal files by bare filename, the jurisdiction file as <slug>/jurisdiction.toml). The layout deliberately mirrors the canopy-rules corpus encoding as an independent reimplementation (KAT-pinned); the two digests hash different inputs and are never compared byte-for-byte. Distinct from the hearing view’s policy_params_digest, which hashes the *household-resolved frozen bundle (varies per household); params_digest is constant per parameter set. The digest covers the bytes actually parsed (single read, digest + parse from the same buffers), so it can never attest bytes other than those scored. Jurisdiction values are witnessed (toml bytes in the digest) but not yet effective-dated — that is #1478.

  • Snapshot schema_version 6, window [5, 6]. v6 adds the all-or-none params_provenance projection ({params_digest, effective_period}); snap emits 6 with it present, the other programs keep emitting 5 with it absent. The Amendment 5 clause "from 5 on the version is independent of which optional projections are present" governed v5’s OWN projections — adding a NEW field to the set is a version bump per this ADR’s refusal doctrine (an old reader must be able to refuse a shape it would silently re-hash differently). v5 rows stay valid and re-verifiable; the floor does not move.

  • Envelope attestation, verifier-tolerant-first, emission-gated. SignableDetermination gains policy_target
    evaluated_as_of (skip-if-none; evaluated_as_of exists because a denied envelope has no effective_date and the snapshot’s as_of is bound but not readable from the envelope alone). Emission sits behind emit_policy_attestation (default false): the scaling runbook’s rolling deploy rebuilds canopy-snap BEFORE canopy-eligibility, and an old verifier meeting the new signed fields would quarantine valid determinations — the runbook orders the flip after the fleet upgrade; the devstack (one atomic deploy) enables it immediately. The v6 snapshot is NOT gated (snap-local surface; an old binary meeting a v6 row refuses, fail-closed).

  • as_of is the policy anchor. Parameter-set selection, benefit-period dates (effective = as_of, expiration/renewal via the pre-existing + Months(n) arithmetic — end-date inclusivity semantics are #1474), the snapshot as_of, and snap.case_closed.closure_date all derive from the typed ApplicationContext.as_of; only determined_at stays wall-clock. The no-as_of fallback is today as a LEGAL date in the jurisdiction’s timezone (legal_today, #1121 — a UTC date is already tomorrow from ~7pm Eastern). A non-fallback as_of requires the exact canopy-eligibility service identity (403 otherwise); expected_policy_target on the request is the pre-write pin (409 policy_target_mismatch before any evaluation or write); GET /v1/params/provenance is the discovery half a dispatcher composes with canopy-rules GET /v1/corpus.

  • Byte retention is git. The parameter files are rulesets-as-code (ADR-003) in a public repo: any digest is reproducible from history, and no runtime flow recalls raw bytes (the #1472 dry-run evaluates staged files; baseline replay uses the frozen bundle) — so no content-addressed byte store is added (review H9, rejected with this rationale).

  • Live evaluation is corpus-pinned. The determine path resolves GET /v1/corpus once per request and pins ALL rules calls (self-employment, each alien check, main) to it, verifying every echo — a mid-request corpus rollout fails closed (503, retryable) instead of signing a verdict scored across corpora; previously the SE echo was discarded entirely and alien/main could diverge silently.

  • Dry-run carries no params_digest until #1472 defines the target-policy echo.

Program plan: the #1213 COLA program (§P1). Related issues: #1467 (this), #1474 (end-date semantics), #1475 (IEVS wiring), #1478 (effective-dated jurisdiction parameters), #1479 (eligibility-side persistence).

Edit this page · default