Plan: October-COLA bulk re-determination — program decomposition (#1213, epic &73)
On this page
- Status
- Context
- Scope
- Design — prerequisites
- P1 — #1467 snap as_of-faithful determination (own plan round; as built, 2026-08-14)
- P2 — #1468 trigger field, verifier-tolerant-first
- P3 — #1469 rules corpus read + pinned provenance
- P4 — #1470 renewals immutable universe snapshot generations
- P5 — #1471 eligibility persistence correctness (benefits interactive too)
- P6 — #1472 dry-run against a target policy
- Design — the #1213 core (spec skeleton for its own plan round)
- Steps
- Files Touched (this docs MR)
- Verification
- Documentation Updates
- As built — the core MR (Step 7)
- Open decisions
Implements ADR-002 Amendment 1 (D1–D10, #1237) as Step 2 of the parent plan. The contract is ratified; this program plan is the byte-level. An external adversarial review (2026-08-13) rejected the original single-MR plan — the central guarantees (correct October policy, exact cohort coverage, replay convergence, interactive priority, bounded retry, safe rollout) required work in five services. This plan is the accepted decomposition: six prerequisite issues + a redesigned core, mirroring the A8b precedent (#1456: prerequisites as separate blocking issues).
Status
| Step | Description | Status |
|---|---|---|
0 |
File #1467–#1472 with blocks-links; refresh #1213 ACs; flip the parent Step-2 row; commit this plan + nav. |
Done (2026-08-13) — this MR |
1 |
#1468 (P2) |
Done (2026-08-13) — #1468 |
2 |
#1469 (P3) rules |
Done (2026-08-14) — #1469 |
3 |
#1471 (P5) eligibility persistence correctness: atomic completion tx (the D10 MUST), typed program-failure classification, idempotent program persistence. |
Done (2026-08-14) — #1471 |
4 |
#1470 (P4) renewals immutable SNAP universe snapshot generations (frozen cohort source, exact reconciliation). |
Done (2026-08-14) — #1470 |
5 |
#1467 (P1) snap as_of-faithful determination: effective-dated parameter selection, |
Done (2026-08-14) — plan round ran (2 internal reviewer rounds + a 28-finding external review, all adversarially verified; scope grew to the §P1 as-built below, weight 5→8); follow-ups #1473–#1479 filed |
6 |
#1472 (P6) dry-run against a target policy |
Done (2026-08-14) — #1472; baseline replay byte-identical (wire-pinned), target arm content-addressed (P1 |
7 |
#1213 core — substrate, driver, consumer, admin surface, fail-closed enact gate. Own plan round against §Core below. |
Done (2026-08-16) — MR !1141 (merge 68bab6c5), after #1473 (MR !1140, merge 25f8614e); as-built record → §As built below + ADR-002 Amendment 3 |
Epic: &73
Issues: #1213 (core) · #1467 #1468 #1469 #1470 #1471 #1472 (prerequisites) · relates #1133
Branch: feature/1213-cola-program-plan (this docs MR); implementation branches per issue
Context
Scale-audit finding H6: a determination is only ever a synchronous, single-attempt, ~30-call HTTP fan-out per household inside one
interactive request (services/canopy-eligibility/src/orchestrator.rs:1398-1412). The mandatory 7 CFR 273.12(e) October COLA
(rulesets/federal/indexing.toml pins snap-cola to Oct 1 with grace_days = 0; GA ≈ 800K households) has no driver.
The first #1213 plan put everything in one MR. The external review found that unshippable, most fundamentally: the October 2026
policy cannot even be represented today — snap boot-loads hardcoded FY2026 parameter filenames
(services/canopy-snap/src/params.rs:107-118), the rules corpus deliberately excludes parameter JSONs
(services/canopy-rules/src/engine.rs:63-66), and benefit dates derive from wall clock (services/canopy-snap/src/determine.rs:377).
A 2026-10-01 bulk run would have signed FY2026 amounts with October-2nd dates — the driver would be theater. The remaining blockers
(replay non-convergence, consumer deadlock, TOCTOU admission, wrong supersession baseline, unsafe signed-schema rollout, un-gated
downstream blast radius) are each owned by a named fix below.
Enrollment apply-semantics stay out of scope (#1133): an already-enrolled household’s re-determination PARKs at enrollment
(services/canopy-enrollment/src/auto_enroll.rs:141-170, the blessed #1130 bridge). The unpark scanner re-offers 50 events/min, so a
full-caseload production enact would accumulate ~800K parks taking days to drain — production COLA enactment at full scale is
therefore operationally gated on #1133. #1213 ships the machinery and small-cohort operation.
Scope
In scope (program): the six prerequisite issues (#1467–#1472) and the #1213 core (cohort substrate, dispatcher/consumer, admin surface with real dry-run, fail-closed enact gate, operator controls, the failure-mode test suite).
Out of scope: #1133 enrollment adjust-vs-supersede + 273.13 adverse-action routing; snap params/corpus unification (P1 adds selection + attestation, not a merged pin); FTI actor-JWT minting for FTI-bearing cohort programs (follow-up filed at core time); snap-side same-context supersession replay (follow-up filed at core time).
Design — prerequisites
P1 — #1467 snap as_of-faithful determination (own plan round; as built, 2026-08-14)
As built (the plan round’s external review — 28 findings, none refuted — widened the original spec; canonical detail: ADR-028 Amendment 6 + ADR-002 Amendment 2):
-
Effective-dated sets: all
snap-{allotments,deductions,income-limits}-*.jsondiscovered at boot, complete triples grouped by_effective_date, typed full-file parsing (floats in money fields fail loud), INTRINSIC validity[start, next-Oct-1)— identity never depends on staged inventory. Expired/gap selection fails closed (422; accountable overrideCANOPY_SNAP__ALLOW_EXPIRED_PARAM_SET). Cross-source agreement checks make a divergent COLA edit a boot error (single-sourcing → #1478). No-as_offallback =legal_todayin the jurisdiction tz. -
Composite target:
PolicyTarget {corpus_hash, params_digest, effective_period}(canopy-common; validated 64-hex newtypes).params_digestcovers the selected triple + budgeting-factors + jurisdiction.toml raw bytes (same-buffer digest+parse). The live path resolves ONE corpus per determination (GET /v1/corpus) and pins all three rules calls, echo-verified (previously the SE echo was discarded and alien/main could diverge). -
Attestation: snapshot schema v6 (
params_provenance, window [5,6]); envelopepolicy_target+evaluated_as_ofgated behindemit_policy_attestation(default false; devstack on; the scaling runbook orders the production flip after the fleet upgrade). -
Request controls: typed
as_of+expected_policy_targeton the snap contract; non-fallbackas_ofrequires the exactcanopy-eligibilityidentity (403); jurisdiction mismatch 422; expected-target mismatch 409 BEFORE any evaluation or write. Discovery:GET /v1/params/provenance(compose with the rules corpus read for the D8 pin — the core consumes both). -
Dates: benefit period anchors to
as_of(arithmetic unchanged; end-date inclusivity semantics → #1474);determined_atstays wall-clock;snap.case_closed.closure_date=as_of. -
FY2027 data files remain a policy-data task audited by
cargo xtask policy audit; tests ship SYNTHETIC fixture tables (services/canopy-snap/tests/fixtures/fy2027/, never underrulesets/). -
Follow-ups filed from the review: #1473 (snap outbox atomicity, blocks the core) · #1474 (expiration-date semantics) · #1475 (IEVS wiring) · #1476 (medicaid ELE closure_date) · #1477 (appeals legal_today) · #1478 (effective-dated jurisdiction params) · #1479 (eligibility-side target persistence).
P2 — #1468 trigger field, verifier-tolerant-first
DeterminationTrigger (kebab-case: cola|fpl|ruleset-migration|change-report|renewal|worker-initiated) in canopy-common;
SignableDetermination.trigger: Option<_> skip-if-none with the §57 byte-identity pins (crates/canopy-signing/src/envelope.rs:637-668).
The field lands in every verifier/emitter struct in this MR; nothing emits it — canonical-bytes reconstruction
(envelope.rs:147-151) drops unknown fields, so emit-first would quarantine valid signatures during rolling restarts
(ADR-028 verifier-tolerant-first doctrine). The core (a later MR) starts
emitting. OpenAPI regenerated for all five program services with response-body pins.
P3 — #1469 rules corpus read + pinned provenance
GET /v1/corpus (service-caller gated) → CorpusInfo { corpus_hash } from engine.corpus_hash(). rules-client gains one evaluate
combining corpus_hash pin + audit + trace (today evaluate_pinned cannot trace and evaluate_with_provenance cannot pin —
pinned bulk evaluation would lose derivation provenance).
P4 — #1470 renewals immutable universe snapshot generations
POST /v1/renewals/snap/universe-snapshots {active_on} materializes the federal universe server-side in one transaction into a
generation table (id, created_at, active_on, row_count); GET …/{id}/rows?after= pages the frozen generation by keyset. Rows
carry certification_period_id, household_id, application_id, determination_id — the establishing determination is the correct
supersession baseline (crates/canopy-contracts-renewals/src/certifications.rs:17-25). Reconciliation becomes exact:
materialized == row_count, fail-closed, immune to live-table churn. Generations reaped after N days.
P5 — #1471 eligibility persistence correctness (benefits interactive too)
-
Atomic completion (the D10 MUST): tx-taking store variants; combined result + request-status flip +
determination.completedoutbox staging commit in ONE transaction (todaycreate_combined_result/update_request_status/create_program_determinationeach take&PgPooland the handler stages the event best-effort post-commit atsrc/api/handlers.rs:144-171). -
Typed failure classification:
ProgramResultgains sanitizedfailure: Option<{status, code}>— supersession-409, terminal-4xx, and transient-5xx become distinguishable (today every program non-2xx is masked into a 200pending_verificationbody embedding the raw upstream body,orchestrator.rs:1470-1479). Raw bodies replaced by allowlisted code + bounded excerpt. -
Idempotent program persistence:
create_program_determinationconverges on replay (ON CONFLICT (id) DO NOTHING+ read-back) — a middleware-cached program response no longer fails the global PK into a spurious pending. -
determination.completedpayload becomes a typed shared DTO incanopy-contracts-eligibility(fields unchanged here).
P6 — #1472 dry-run against a target policy
DryRunRequest gains optional target_policy { corpus_hash, params_digest } (absent ⇒ existing baseline-replay, byte-identical).
With a target: rules pinned via P3, snap params selected via P1; response carries baseline + target outcome/amount and echoes the
resolved target. Write-free end to end. This is what makes the admin preview the "dry-run" the parent contract requires.
As-built (2026-08-14, #1472): the wire ref is canopy-common::policy_target::PolicyTargetRef (validated hex64 pair). Snap’s
DryRunDetermineRequest models the choice honestly — the baseline fields became Option and resolve_dry_run_policy enforces
exactly one source (400 otherwise; no ignored-but-required fields). Param selection is find_by_digest — content-addressed and
date-blind (a staged next-window set is previewable before October 1; the live path’s expired-set guard stays on the live path).
The target arm additionally requires an explicit context.as_of (400 — no implicit evaluation date) and the configured
jurisdiction (422), and an unknown digest fails closed 422. The echo is snap’s full resolution (PolicyTarget incl. the matched
set’s intrinsic window) on both the snap outcome (resolved_target) and the eligibility result (target_policy) — exactly the
value the core later passes as expected_policy_target. Target mode resolves the baseline verdict + application id WITHOUT the
frozen-bundle extraction, so corpus-less / pre-T2-7 baselines still preview under a target (the bundle-extraction 422s are
replay-arm-only; a snapshot-less baseline 422s in either mode — the snapshot supplies the application id).
Design — the #1213 core (spec skeleton for its own plan round)
Frozen by the approved program plan; the core’s plan round turns each into files/DDL/signatures.
Retry ownership. Case rows own ALL retries; MQ is a trigger only. The consumer handler never returns Err for business outcomes
(only malformed payloads DLQ). No FOR UPDATE held across HTTP (the reviewed deadlock: handler-tx row lock + second-connection
bookkeeping). Execution claim = CAS on a short separate connection (… WHERE state='dispatched' AND dispatch_generation=$n AND
attempt_started_at IS NULL); CAS miss ⇒ ack no-op. Scheduling lives in next_attempt_at (exponential + jitter); the dispatcher
republishes. Dissolves the MQ-attempts-vs-case-attempts contradiction and RabbitMQ’s immediate-requeue-no-backoff.
Dispatch generations fence everything. dispatch_generation on the case, stamped into every event; consumer CAS requires a match;
re-arm bumps it (stale queued duplicates are no-ops); in-flight accounting counts the current generation.
bulk_redispatch_ttl_secs boot-validated > max_inflight × self_call_timeout / consumer_concurrency.
Entry idempotency — generation-scoped natural key. At first dispatch the case freezes its canonical DetermineRequest
(request_body JSONB + hash); every attempt resends identical bytes. eligibility_requests += bulk_case_id, bulk_attempt_generation;
unique partial index on (bulk_case_id) WHERE status IN ('pending','in_progress'). Resume: a completed request from the current
generation with a definitive result ⇒ succeeded (replay from persisted rows); non-definitive ⇒ bump generation, re-execute. The
self-call sends no idempotency-key header — eligibility IS wrapped by the optional #1003 middleware
(crates/canopy-api/src/lib.rs:191-195 + the #1463 fleet schema), and a keyed state-dependent 409 would be cached replayable for 24h
(idempotency.rs:1052-1060). The D4 hash key lives at the per-program layer, keyed over frozen body + policy target + generation.
Outcome classification is body-parsed (P5’s typed failures): succeeded requires every requested program definitive
(approved/denied bucket, determination_id: Some); pending/quarantined ⇒ transient; supersession-conflict ⇒ skipped_superseded;
terminal-4xx ⇒ failed_terminal; 408/429/5xx/transport ⇒ transient. Bulk suppresses register_pending_verifications entirely (D6).
Interactive priority — serialized admission. create_eligibility_request takes pg_advisory_xact_lock(hashtext(household_id)) on
both paths; the bulk arm probes for ANY live request for the household (sibling applications included — the
(application_id, household_id) index doesn’t fence them) with the 5-minute staleness qualifier, under the lock. No TOCTOU. Recorded
residual: an interactive request arriving while a bulk determination is already executing sees today’s standard in-progress 409
(bounded seconds); "interactive always wins" = wins every admission race, never queues behind bulk.
Supersession baseline. Cases persist baseline_determination_id from the P4 snapshot row. A typed supersession-conflict ⇒
skipped_superseded (own terminal state, counted separately) — an intervening successor post-flip already carries the new policy;
pre-flip intervening successors surface on the failures list for re-run. No blind retries against snap’s one-successor constraint.
Signed-provenance validation. Bulk arm rejects as contract_violation (terminal) any envelope whose trigger, policy target, or
previous_determination_id mismatch the dispatch context — an old snap that ignored the new fields cannot silently pass.
DetermineRequest.bulk requires claims.service_id() == Some("canopy-eligibility") (exact identity); BulkDispatchContext carries
cohort_case_id + dispatch_generation, validated against the durable case row.
Provenance persisted. cohort_case_attempts (case, generation, eligibility_request_id, outcome, error code/excerpt,
started/finished) — attempt history, never overwritten. Program rows (P5) + snap persist trigger and the policy target; replay
reconstruction reads persisted rows.
Side-effect blast radius — fail-closed with accountable override. bulk_runs_enabled default false. Enact additionally
requires CANOPY_ELIGIBILITY__BULK_ENACT_ACCEPT_DOWNSTREAM=true; the refusal names what enact unleashes pre-#1133 (per-case
determination.completed.snap → notices NOAs, ELE, Medicaid granting-source effects, enrollment parks at 50/min drain). Fail-closed
default + explicit per-control accountable override, per the ADR-041 doctrine. A doc warning is not a gate; this is a gate.
Worker pass order + fencing. Per pass: (1) deadline/finalization, (2) enacting dispatch, (3) re-arm, (4) materialization — an
upstream outage can’t starve finalization. Per-run error isolation; run-row FOR UPDATE around cursor/count writes with
expected-value predicates. Run-level circuit breaker: failure ratio over a sliding window ⇒ run paused (operator resumes).
Startup/shutdown/readiness. Subscribers start only after the HTTP server is ready (bind-then-subscribe); disabled ⇒ never subscribed; consumer handles + worker registered for graceful shutdown; readiness exposes worker and subscriber liveness (non-gating).
Pool + concurrency honesty. The dedicated 2K+2 pool serves consumer bookkeeping/inbox only; the loopback handler intentionally
uses the interactive pool — K bounding concurrent self-calls IS the contention control. Boot validation
db_max_connections ≥ 2K + 4. K is per-replica; replicas share work via SKIP-LOCKED dispatch + competing consumers.
CANOPY_MQ_PREFETCH_COUNT asserted ≤ 4 at boot when consumers are enabled (never a silent 32). Shared program circuit breakers:
breaker-open ⇒ transient ⇒ the run breaker pauses the run rather than burning attempts.
Scale is measured, not claimed. Throughput = replicas × K ÷ mean per-case latency; the run deadline is a knob. Acceptance: a seeded soak (≥5K cohort on devstack) measuring throughput + interactive latency under drain; a backlog-slope metric + alert; a production sizing table in the runbook.
Operator surface. GET …/bulk-runs/{id}/failures (paginated); POST …/pause, /resume, /cancel, /retry-failures (bounded
reset, bumps generation); status = per-state counts, throughput, ETA, breaker state. Retention reaper for terminal
cases/attempts/entry keys.
Run/state invariants. Finalization requires succeeded + failed_terminal + skipped_superseded + canceled = materialized_count;
failures ⇒ completed_with_failures, clean ⇒ completed. Completed-target re-run needs allow_rerun=true; enact during another
enact / on a failed run ⇒ 409 naming it; Location on 202s; BulkRunAccepted.request_id named per D10’s wording. Clients must not
send idempotency-key headers on bulk-runs POSTs (keyed state-dependent 4xxs cache 24h).
Create contract restricted (v1). trigger must be cola; programs must be [Program::Snap] (canopy_reference::Program,
duplicates rejected); as_of required, validated against the snap-cola cutover window. A signed change-report trigger that
actually ran a COLA scan would be cryptographically authoritative and semantically false.
Principals split (D3/D8). Run persists created_by, enacted_by, and the captured authz basis separately; per-case
requested_by = system:cola-redetermination:<run> because the cohort is machine-selected — the D3 "real originating worker" clause
is satisfied at the run level. FTI actor-JWT follow-up stands.
Config truth table. bulk_runs_enabled=false ⇒ no subscribe, no worker, 503 on POSTs (the only dormant state). =true with any
URL/knob missing or out of range ⇒ boot error — never silently dormant. Cross-field checks: the TTL inequality, the prefetch
assertion, the pool floor.
Steps
Step 0 is this MR (see Status). Steps 1–4 (#1468, #1469, #1471, #1470) are implementable directly from §Design — prerequisites; each lands as its own MR with its issue’s acceptance criteria as the test bar. Step 5 (#1467) and Step 7 (#1213 core) are design-bearing and get their own plan rounds; the core plans against §Design — core, and its verification carries the review’s failure-mode suite verbatim: crash-after-program-row, middleware cache expiry, same-key-different-body, intervening successor, churn between preview and enact vs the frozen snapshot, outage longer than the redispatch TTL, worker lock loss, startup with a preloaded queue, disable with queued work, all-cases-failed finalization, canonical replay, rolling-restart signature tolerance.
Files Touched (this docs MR)
| File | Change |
|---|---|
|
This program plan (new; since moved to |
|
Step-2 row → |
|
Nav entry under Scale Readiness (epic &73). |
Verification
-
cargo xtask plan-lint— Status vocabulary clean. -
cargo xtask check-docs+ Antora xrefs resolve (this plan ↔ parent ↔ ADR-002 A1 ↔ ADR-028). -
#1467–#1472 filed with type/priority labels, weights, T1 milestone, epic &73, blocks-links to #1213 (P1/P3 additionally block #1472); #1213 ↔ #1133 related.
-
#1213 description carries the refreshed AC 5 and the blocked-by list.
-
Docs-only MR — no functional battery required beyond the pre-push hook.
Documentation Updates
-
This plan nav-linked under Scale Readiness (epic &73).
-
Parent plan Step-2 row updated.
-
CHANGELOG.adoc— not warranted (plan/docs only; no behavior change). -
Per-service api/data-model pages — owned by the implementing MRs (P2–P6, core).
As built — the core MR (Step 7)
Status: Done (2026-08-16) — the core landed as one MR (!1141, merge 68bab6c5: contracts/common, snap
delta, cohort substrate, bulk arm, consumers + worker + config, admin surface, the live AC6
failure-mode suite, these docs), after #1473 shipped as its own MR (!1140, merge 25f8614e). The
as-built record — the epoch/ledger split, refinements 1–8, and every deviation from this plan’s
§Design — core skeleton — is
ADR-002 Amendment 3; operations live in the
bulk COLA scaling runbook. Follow-ups filed at implementation
start: #1480 (pending-verification dedup), #1481 (breaker half-open CAS), #1482 (re-baseline superseded
cohort cases), #1483 (FTI actor-JWT).
Open decisions
All review findings are folded in; three deliberate deferrals are recorded here rather than silently dropped:
-
FTI actor-JWT for FTI-bearing cohort programs — filed as #1483 at implementation start (SNAP has no ADR-014 chain; D3’s FTI clause is unexercised by the COLA driver).
-
Snap-side same-context supersession replay (D4-honest 200-replay on supersession conflict) — resolved in the as-built core with NO snap change: the bulk arm’s adopt-or-skip (ADR-002 Amendment 3, D-2c) recovers our own lost determination from the successor’s signed provenance on the conflict 409, so the honest replay lives client-side; the genuine-intervening-successor residual is #1482 (re-baseline).
-
Full-caseload production enact is gated on #1133 (park-scale math in Context); the enact override gate makes this operational fact a hard control, not a doc note.