Plan: October-COLA bulk re-determination — program decomposition (#1213, epic &73)

On this page
NOTE

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) DeterminationTrigger in the signed envelope, verifier-tolerant-first — field lands fleet-wide, nothing emits.

Done (2026-08-13) — #1468

2

#1469 (P3) rules GET /v1/corpus + pinned-provenance evaluate (pin+audit+trace in one call).

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, params_digest attestation, as_of benefit dates. Design-bearing — own plan round.

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 {corpus_hash, params_digest} (the real COLA preview). Depends on P1 + P3.

Done (2026-08-14) — #1472; baseline replay byte-identical (wire-pinned), target arm content-addressed (P1 find_by_digest + P3 pin), full resolved-target echo (see §P6 as-built)

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}-*.json discovered 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 override CANOPY_SNAP__ALLOW_EXPIRED_PARAM_SET). Cross-source agreement checks make a divergent COLA edit a boot error (single-sourcing → #1478). No-as_of fallback = legal_today in the jurisdiction tz.

  • Composite target: PolicyTarget {corpus_hash, params_digest, effective_period} (canopy-common; validated 64-hex newtypes). params_digest covers 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]); envelope policy_target + evaluated_as_of gated behind emit_policy_attestation (default false; devstack on; the scaling runbook orders the production flip after the fleet upgrade).

  • Request controls: typed as_of + expected_policy_target on the snap contract; non-fallback as_of requires the exact canopy-eligibility identity (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_at stays 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 under rulesets/).

  • 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.completed outbox staging commit in ONE transaction (today create_combined_result / update_request_status / create_program_determination each take &PgPool and the handler stages the event best-effort post-commit at src/api/handlers.rs:144-171).

  • Typed failure classification: ProgramResult gains sanitized failure: Option<{status, code}> — supersession-409, terminal-4xx, and transient-5xx become distinguishable (today every program non-2xx is masked into a 200 pending_verification body embedding the raw upstream body, orchestrator.rs:1470-1479). Raw bodies replaced by allowlisted code + bounded excerpt.

  • Idempotent program persistence: create_program_determination converges 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.completed payload becomes a typed shared DTO in canopy-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

docs/modules/ROOT/pages/plans/scale-audit-1213-cola-program.adoc

This program plan (new; since moved to plans/archive/ on completion).

docs/modules/ROOT/pages/plans/scale-audit-adr002-async-bulk.adoc

Step-2 row → Blocked (#1467–#1472 — this program), pointing here.

docs/modules/ROOT/nav.adoc

Nav entry under Scale Readiness (epic &73).

Verification

  1. cargo xtask plan-lint — Status vocabulary clean.

  2. cargo xtask check-docs + Antora xrefs resolve (this plan ↔ parent ↔ ADR-002 A1 ↔ ADR-028).

  3. #1467–#1472 filed with type/priority labels, weights, T1 milestone, epic &73, blocks-links to #1213 (P1/P3 additionally block #1472); #1213 ↔ #1133 related.

  4. #1213 description carries the refreshed AC 5 and the blocked-by list.

  5. 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:

  1. 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).

  2. 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).

  3. 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.

Edit this page · default