Plan: Adverse Actions, Hearings & the 273.15(k)/PAMMS Pipeline (epic &72)

On this page
NOTE

Umbrella: #1084 (the inert continued-benefits hold) — which two external review rounds (~90 findings) traced to a missing causal identity: appeals joined to terminations by household + timing coincidence. This plan (rev 6 of the design) rebuilds on an adverse_action_id spine, absorbs the core of #1002 (an appeal must bind to a real action), and grounds every policy ruling in rulesets/georgia/.policy-cache/dfcs-snap/ with citations — revision 5 encoded priors that contradicted PAMMS 3730 and Appendix B.

Deliverable definition: #1084 closes at a clearly labeled UAT-only milestone. The Production-Gap Register lists every known gap between UAT-grade and production-grade, each with a tracked successor issue. Issues are cut from the Status rows per ADR-013; epic &72 groups them.

Status

MR Description Status

MR 0.1 (plan + tracking)

This document, nav-linked Active; epic &72 + child issues per Status row incl. the four live today-bug issues (transient subscriber queues; notices hardcoded recipient + no-op delivery; retained-predicate lower bound; rolling-deploy/unbound-queue event discard) and the production-gap register successors.

Done (2026-07-19) — !881; epic &72, issues #1087–#1117; two pre-commit J-flags fixed (present-tense gap claim, auto-close keyword)

MR 0.2 (durable broker topology)

fix: durable subscriber queues in canopy-mq (queue survives broker restart — regression-tested in devstack); RabbitMQ data volume in docker-compose; queue pre-declaration in devstack definitions (or publisher mandatory + alternate-exchange capture) so startup events cannot be discarded pre-binding.

Done (2026-07-19) — #1088: durable-by-default subscribe() + 406-scoped transient→durable self-heal; broker volume + pinned hostname; canopy.unrouted alternate-exchange capture (policy, so code-side declares stay arg-compatible); declare-equivalence + self-heal regression tests in the default battery; the opt-in restart suite now exercises plain subscribe()

MR 0.3 (consumption + deployment protocol)

Parked-state inbox (status, schema/capability version, claim lease, attempts, unpark scanner, targeted replay-to-queue by event id); envelope schema_version + additive-compatibility rule; queue migration protocol (versioned names, unbind, zero-depth/in-flight checks, DLQ preservation, rollback); binding-first deployment rule — every later phase splits topology/consumer MRs from producer-activation MRs — with an xtask lint.

Done (2026-07-19) — #1089: ParkEvent + parked inbox rows (park/repark/claim with FOR UPDATE SKIP LOCKED pass-cursor) + per-subscriber unpark scanner + on-demand run_unpark_pass; classify row-lock kills the InFlightRetry double-run; envelope schema_version (legacy defaults to 1, proptest-pinned); inbox schema single-sourced with the outbox (ADR-039 generator, incl. 5 publish-only services); cargo xtask mq-topology binding-first gate (battery stage 9e) over a 69-key honest allowlist register; event-delivery-protocol.adoc (dispositions table, migration procedure, provenance caveat)

MR 0.4 (retained predicate)

fix: closure-month scoping for the retained flag (benefit_month >= date_trunc('month', retained_through)) + multi-month integration test. Latent today; live the moment terminations exist — precedes all entity work.

Done (2026-07-19) — #1090: lower bound added to the #447 LEFT-JOIN predicate; pinned by a three-month black-box test (June/July issued+unretained, August closure-month retained) against the rebuilt service

MR 0.5 (notices execution reality)

Work-item pattern (subscriber tx persists the item; persons/render/scan/upload run OUTSIDE any tx; fenced short commit; delayed retry + reconciliation, not 5-fast-to-DLQ). Real recipient resolution via canopy-persons (household-membership validation for overrides; mailing-address precedence; redacted/incomplete addresses rejected). Render/upload failure BLOCKS (no PDF-less rows satisfy anything). Delivery outbox: dispatched_at (provider-accepted) vs delivered_at (confirmed); best-effort dedup honestly labeled. Gated-clock dates; render context built AFTER clamping (PDF == stored row).

Done (2026-07-19) — 1091: notice_work_items queue (UNIQUE (source_event_id, notice_type); enqueued atomically with the inbox row); worker with claim-fenced complete/fail (claimed_by+claimed_at — lease theft cannot double-mint, live-DB-pinned), exponential due_at backoff, terminal failed at 8 attempts with last_error; recipient resolution via canopy-persons (membership-validated, mailing-first, redacted/incomplete rejected); NoticeGenerator split prepare/persist; render failure BLOCKS (deterministic 400 on the direct POST’s unknown key); dispatcher stamps dispatched_at + notice.dispatched in one fenced tx (allowlisted pending the MR 3.1 consumer); notice_date on the gated decision clock (B8 held via the persons resolve_as_of tier-2 rewrite); en-route fix: mq-topology no longer truncates at outline [cfg(test)] mod …; declarations

MR 0.6 (issue_benefits guard)

fix: status guard (pending_issuance|active), standalone per house rule; black-box test.

Done (2026-07-20) — #1092: 409 guard after the enrollment fetch (before the pending-issuance row / EBT call, so the rejection is row-free); expedited pending_issuance flow preserved; black-box create→terminate→issue pins the 409 + zero issuance rows

MR 0.7 (broker identity)

Per-service RabbitMQ principals with publish/consume ACLs — envelope source_service is labeling, not authentication. Descoping this moves it to the production-gap register by explicit decision.

Done (2026-07-20) — #1093: 18 canopy-<service> principals (scoped resource perms + per-service topic-write regex on canopy.events, generated from the mq-topology publish map; ^$ for publish-nothing services); broker-refused forgery pinned by canopy-mq/tests/acl_test.rs incl. channel-kill isolation and the admin-principal tooling invariant; replay↔ACL interaction documented (foreign-key replay → failed bucket) + follow-up issue filed

MR 0.8 (security audit queue)

fix: canopy-security audit subscriber: durable named queue; persistence failures propagate (no swallow-and-ack).

Done (2026-07-20) — 1094: the audit subscriber moved off subscribe_exclusive onto the #1088 durable-named-queue path (events buffer while the service is down; live queue properties pinned by a mgmt-API probe); extracted persist_audit_event propagates write failures (nack → DLQ), pinned by sabotaged-table fault injection

MR 1.1 (adverse-action entity)

enrollment_pending_terminations (the action; its id IS the adverse_action_id) + adverse_action_notices versioned child (legal DATES in America/New_York; the action’s current legal dates = latest DISPATCHED version; late-notice repair = successor version with new dates); termination_appeal_links (full constraints, monotonic transitions) + append-only action_signals; policy snapshot (policy_version, required advance days, CB rule per P4); source_generation + UNIQUE(created_source, source_reference, source_generation); exemption authority/actor; recipient snapshot. Enrollment row gains lifecycle_revision — the shared fence: terminate bumps it, mark_issued preconditioned on it (EBT-resurrection race dies). benefit_month first-of-month CHECK + API validation. Terminable-state matrix. Direct terminate endpoint REMOVED (410).

Done (2026-07-20) — #1095: all four tables + trigger landed; store-level enact_termination is the interim only-path-to-terminated (matrix + lock order enforced; the Phase-3 guarded enact wraps it at MR 3.2); fenced mark_issuance_issued_fenced wired into issue_benefits (race pinned by an interleaving test); proration re-anchored on the newly persisted application_date, application-month-only per 7 CFR 273.10(a)(1)(ii) (the request’s day-of-month is no longer a caller money knob); enactment already gates on zero stays / no veto; first-issuance activation fenced like the mark; first-of-month CHECK is NOT VALID for pre-existing devstack seeds (generator normalized; ADR-016 forward-only); the inert suspend-for-appeal machinery + its subscription deleted; terminate-dependent black-box tests moved to SQL-arrange until MR 3.2 (journey-recovered at 6.1); ApiError::Gone added for the explanatory 410

MR 1.2 (action API + stay commands)

Schedule/list(cursor-paginated)/cancel (moots links atomically; idempotency keys; actor required); internal lookups for appeals (GET /v1/adverse-actions/{id}, household-scoped open list — service-caller); synchronous fenced stay/release/veto commands (PUT /v1/adverse-actions/{id}/stays/{appeal_id} with receipt) — the grant-vs-sweep race closes at filing. NO events emitted yet (topology-first). Contracts + roundtrips; full CreateEnrollmentRequest HoH fan-out (canopy-web, appeals tests, household-issuance tests, enrollment tests, roundtrips, test-lib).

Done (2026-07-20) — #1096: all five endpoints landed (schedule snapshots policy per P4 + P8 one-open-action; cancel moots links atomically; stay receipts fenced in one tx with veto mooting the action); HoH column + full constructor fan-out (canopy-web resolves the head from the persons household-full view); notice-version supersession machinery gained its lifecycle test; renewals list_due tier-2 as_of offsets the new gated-clock read (B8 held)

MR 1.3 (HoH backfill)

One-shot canopy-persons backfill of head_of_household_person_id for existing enrollments + auto_enroll threading; Phase-5 automation refuses on residual nulls.

Done (2026-07-20) — #1097: cargo xtask backfill-hoh (report-only default; --apply fill-only guard; residual-null report names the remediation) resolving heads via the persons membership predicate; devstack run 13/13 + idempotent; seed generator emits HoH so reseeds never recreate the debt; auto_enroll threading landed with #1096; the 422 schedule guard is the black-box-pinned interim automation guard

MR 2.1 (appeals: action-bound filing + CB election)

Every appeal of an existing adverse action REQUIRES its action id (validated via MR 1.2 lookups; server-stamped request_date via gated clock — web backdating dies); narrative grievances never carry CB. CB election per P1: 14-day window from notice date (timely/adequate), assume-continuation-unless-waived (Form 118), good-cause flag, reinstate-within-5-working-days command, repayment disclosure recorded. Synchronous stay BEFORE the CB grant commits; enrollment-unavailable → pending_stay + retry worker. Typed program enum. Duplicate appeals: veto absolute; action proceeds only at zero active stays; liability per-appeal. Until the Phase-3 notice machinery stamps dispatched-notice evidence, the election window anchors on the CONSTRUCTIVE notice date (enact_not_before − required_advance_days; enact_not_before itself for exempt actions; never-noticed = timely) — MR 3.1 upgrades the anchor to real dispatch dates.

Done (2026-07-20) — #1098: filing rewritten around the Chart B2 election engine (pure evaluate_election + proptest’d 5-working-day math); pending_stay two-step commit (grant ONLY with the fenced stay receipt, persisted; retry worker under advisory lock; inline first attempt at filing) — the cross-service suite pins the receipt on BOTH sides (granted_timely + active_stays on the live action); good-cause-late recorded as pending_good_cause for OSAH (MR 2.2 resolves); GENERATED eligibility column replaced + backfilled; deny_unknown_fields pins backdating dead on the wire; canopy-web form drops its date fields (action-selection UI lands at MR 3.3, so web files narrative grievances until then)

MR 2.2 (appeals: decision + withdrawal lifecycle)

Decision records signed + received dates (validated, audited, actor-required); agency-favorable → schedules decrease/closure for the NEXT issuance cycle after receipt + adequate notice (successor notice version; NO immediate enact) per P3. Withdrawal state machine per P12 (oral/written, confirmation notice, 10-day reinstatement, finalization releases the stay). Dismissal typed basis. Decision vocabulary migration for legacy rows (map/quarantine). P13: decision_clock 90→60 + due-date recompute + postponement extensions. CB cessation records per P2 (typed reason + actual date; final-appeal 30-day extension per Chart B3). Events pinned (appeal_id, action_id, household_id, program, decision, signed/received dates, schema_version) — emitters activate only after Phase-3 consumers deploy. The recorded next-cycle date + cessation ARE the Phase-3 successor-notice inputs (appeals cannot mint enrollment notice versions; MR 3.1’s consumers do, from the pinned decision event).

Done (2026-07-20) — #1099: typed decision vocabulary (legacy upheld/reversed mapped, unknowns quarantined with the raw value preserved); Chart B1 wired cross-service — reversal = VETO, agency-favorable = stay RELEASE + P2 cessation at the next issuance cycle (receipts persisted; failures routed to the 2.3 scanner); the P12 withdrawal machine landed with finalization as the only stay-releasing transition (release receipt precedes the commit; early finalize refused while the household’s 10-day window is open); decision_clock_days corrected 90→60 with open due dates recomputed + appeal_postponements; final_appeal_window_days = 30 (Chart B3) keyed + cited for the final-appeal machinery; the three Phase-3 payloads pinned with schema_version (unpublished); the pre-#1099 best-effort decision-event staging folded into the decision transaction

MR 2.3 (appeals reconciliation skeleton)

Scanner: stay receipts vs enrollment links disagreement; CB elections parked in pending_stay past SLA → alert.

Done (2026-07-20) — #1100: nightly advisory-locked sweep + on-demand internal trigger returning a typed report; enrollment gained the per-appeal link lookup (GET …​/stays/{appeal_id}, receipt-shaped, service-caller) as the ground truth; parked-election SLA keyed as the cited-operational pending_stay_alert_hours (24h); report-only by design (per-row enrollment outages counted as skipped_unreachable, never guessed); a dedicated lingering-stay sweep (terminal appeal + live link — both sides AGREE, so the equality sweep structurally cannot see the #1099 failed-post-commit residue; the lifecycle-vs-link predicate can); runbook appeals-reconciliation.adoc covers all three alerts

MR 3.1 (action notices)

Manifest routing (BOTH rulesets) with typed effective_date_policy; CB deadline rendered + persisted from the action’s notice version (P1 rule, inclusive semantics defined); notice rows persist action id/version/reason/exemption; currency formatting; DHS-333 verification task. Enrollment consumers for notice.generated/notice.dispatched stamp notice-version evidence on the action.

Done (2026-07-20) — #1101: the enrollment.adverse_action_scheduled route (payload pinned as AdverseActionScheduledV1; producer activates at MR 3.2) renders noa-termination VERBATIM (typed effective_date_policy, clamp preserved for every legacy route); the CB deadline is notice_date + continued_benefits_election_days INCLUSIVE, read from the SAME [appeals] key appeals enforces (no drift), rendered + persisted; notice rows carry the binding (action id, source generation, reason, exemption); evidence events feed order-independent enrollment upserts (partial UNIQUE on action+notice; dispatch supersedes) — pinned end-to-end by an injected-event black-box test (an EXEMPT action effective today — strictly inside the clamp floor, so the assertions discriminate verbatim from clamp) through render → dispatch → adverse_action_notices, with store-level order-independence + supersession + idempotent-replay pins. DHS-333 RESOLVED: Form 333 is the Sanction/Penalty notice (CONFIRMED on sanction); the general adverse-action notice is system-generated per 3705:43 → fabricated labels removed. Currency arrives pre-formatted from the pinned producer payload (documented)

MR 3.2 (the enact primitive + consumers + activation)

ONE guarded enact path for sweep/decision-scheduled/withdrawal-finalized/immediate-exempt: verifies action state + zero active stays from ANY appeal; dispatch evidence for the CURRENT notice version (render/upload success + dispatched_date ≤ noticed_effective_date − required days, jurisdiction-tz DATE math; exempt = dispatched adequate notice); lifecycle_revision fence; enact_not_before re-read under the global lock order. Failure → successor notice version (new legal dates) + alert. Sweep + on-demand trigger. Enrollment consumers for Phase-2 appeal events (order-independent link upserts + tombstones). Producer ACTIVATION of enrollment events is the LAST MR-step, after 3.1/2.3 consumers deploy.

Done (2026-07-20) — #1102: guarded_enact + enact_gate_locked (stays/veto → current-window dispatched evidence → advance-days or exempt-adequate → no-enact-late month window → enact_locked with terminated_date = the NOTICED date + PAMMS 2415 retention + the lifecycle fence); repair mints the successor window (enact_not_before re-anchor + notice_repair signal fence + re-published scheduled event — repair IS the normal pipeline) with PendingEvidence suppressing repeat mints; hourly advisory-locked sweep + POST /v1/adverse-actions/enact-sweep (202 on lock contention, typed EnactSweepReport); apply_appeal_resolution consumes the ACTIVATED appeal.decision_recorded/appeal.withdrawal_finalized inside the inbox tx (read-your-writes with the gate): reversal = durable veto + tombstone (late grant refused; post-enactment reversal signals Phase-4 restoration), upheld-with-cessation reschedules to next cycle as exempt-with-authority (successor letter drops CB per P3), withdrawal releases + gates. Legacy decision events DELETED; enrollment.adverse_action_scheduled producer ACTIVE (payload + created_source/source_reference; staged in the schedule tx; broker ACLs updated); jurisdiction-tz legal dates in enrollment (crate::clock::legal_today) AND notices (dispatched_date). Organic pipeline E2E (schedule → notice → dispatch → evidence → sweep → terminated) + live reversed-decision→veto convergence + 12 store-level gate/convergence pins

MR 3.3 (worker surface)

canopy-web schedule/cancel action: authz + household anti-tamper + verified actor propagation; exemption restricted by role; reason picker from policy TOML; action-selection UI for appeal filing; scheduleTerminationViaUi helper; negative tests.

Done (2026-07-20) — #1103: actions_snap_adverse_action module (WritePermission + program scope + server-resolved enrollment + neutral-mismatch cancel anti-tamper; actor = authenticated worker); vocabulary in BOTH rulesets, boot-loaded (ActionReasons, fail-fast) and re-validated server-side; operator-tier exemption (template gate + server role re-check + pair-required authority); Determination-tab scheduled-actions read-back (labels, legal date, exemption/stay badges, per-action cancel); #974 filing gains validated action selection + waiver/disclosure checkboxes (Chart B2 rides the action-bound filing); e2e: scheduleTerminationViaUi/cancelTerminationViaUi, an end-to-end spec incl. CB-granted stay badge + forged-exemption/tamper negatives, backdating-era helper surface removed (upheld-decision journey’s CB leg explicitly deferred to the MR 6.1 recovery)

MR 4.1 (assessment + claims schema)

cb_assessments (window = CB start … cessation date per P2; HoH snapshot immutable; retention snapshot = lifecycle_revision + retention fields; post-decision issuances ITEMIZED per P7) + cb_assessment_lines (per-issuance allocation, P6) + assessment_work (UNIQUE per appeal, status, attempts, due, lease/fence, heartbeat, error, backoff, pruning). Claims: appeal/action/source_event ids + real UNIQUEs; liable person = HoH snapshot; append-only claim_adjustments with status/balance recompute, repayment-plan interaction + refund/credit ops-flag; void path for veto/cancel-after-assessment.

Done (2026-07-21) — #1104: the entity trio landed with P6 AS A CONSTRAINT (partial UNIQUE (issuance_id) WHERE disposition = 'billed' across ALL assessments; void releases lines in the same tx so a successor can bill) + one-active-per-appeal partial UNIQUE (supersession = void + successor) + the P7 itemized disposition for post-cessation lines. record_decision phase 3 records the entity inline as the INTERIM writer (liable person = the action’s recipient snapshot; retention snapshot via a new appeals→enrollment get_enrollment lookup; savepoint-guarded idempotent replay on decision re-record; degraded paths — legacy grants, unreachable snapshots — keep the claim event and WARN naming the 4.2 backfill) and the claim amount became the P2 window figure (post-cessation issuances itemized, no longer billed as if in-window). Zero-dollar assessments persist as computed rows with a $0 display projection and NO claim event. Claims: provenance quad + partial UNIQUEs (redelivered appeal.overpayment_assessed is structurally a no-op — all three program subscribers stamp it), void status (sticky; balance zeroed by a compensating adjustment; recouped-money flags refund review), append-only claim_adjustments with close/REOPEN recompute (closed is derived, not a ratchet). Canonical migration stamped ×3 with byte-parity crate-tested. 36 tests incl. the full invariant matrix against the devstack.

MR 4.2 (assessment worker + fences + activation)

Work item from inbox tx; HTTP outside; issuance finality fence (pending EBT → blocked + re-queued); apply fenced on (status, lifecycle_revision, allocation-set version); household-scoped allocation serialization with full-set recompute on corrections (P6 total order: decision_signed_date, filing date, appeal_id). Scanners cover ALL liable dispositions (upheld/withdrawn-finalized/dismissed); pre-cutover backfill; downstream claim-acknowledgment scanner; inline phase-2 assessment deleted in the activation step.

Done (2026-07-21) — #1105: the worker drains assessment_work (SKIP LOCKED + 10-min stale-lease reclaim; attempts counted at claim; 8-attempt budget with exponential backoff; failed = operator surface) on appeals' own ADR-019 service identity (mandatory at boot; EnrollmentClient collapsed to service-token-only — the bearer mode died with the inline writer); HTTP snapshots outside any tx; ONE short apply tx under the per-household advisory lock re-validates the appeal, records a derived cessation when backfilling (P2 record-then-assess), resolves P6 by the total order (earlier holder keeps → already_billed; later holder voided reallocation + lines released + appeal re-queued; ties break on appeal_id), supersedes this appeal’s own stale assessment (allocation_version+1), and completes the work item. Withdrawal finalization became a first-class liable trigger: records cessation (withdrawal_finalized added to BOTH cessation-reason CHECKs) + enqueues in the fenced tx; decisions enqueue in the decision tx (dismissed typed separately). Ack loop event-driven: snap.overpayment_claimed (+appeal_id/assessment_id) consumed on canopy-appeals.claim-acks flips computed → applied; zero-dollar applies immediately. Hourly advisory-locked scan = backfill sweep (liable + no assessment + no work item + overpayment_amount IS NULL so legacy-claimed degraded rows never mint a second claim) + unacked re-emit (snap select-firsts on assessment_id and re-acks instead of DLQ-ing) + dead-action sweep (vetoed/cancelled → re-queue → worker voids + pinned appeal.overpayment_assessment_voided voids the downstream claim ×3 via stamped-store void_for_assessment). Interim inline writer DELETED. Tests: lease/defer/requeue lifecycle, ack idempotency, pure allocation matrix incl. same-day ties, household-lock TOCTOU, zero-dollar concurrency convergence, displacement end-to-end, snap void/re-ack convergence.

MR 5.1 (periodic-report cohort + state model)

periodic_report_required cohort per P11 (2026-03-02 phase-out; legacy extended-cert cohorts only; Senior SNAP waiver) + migration backfill. PR state machine (notice_sent_15th; form complete/incomplete per 3730:35; VCL sent/10-day due; discrepancies; verified; processed). Scheduler predicate COMPLETE (cohort, status, active, non-null dues, calendar cutoffs, bounded batches). Completion + change-report writes in one locked tx with outbox events.

Done (2026-07-21) — #1106: cohort flag decided by the SAME predicate at creation and in the marker-delimited migration backfill (interval comparisons, not whole months — a 6.5-month cert is "longer than 6 months" per 3730:18; senior waiver = 36-month duration proxy documented against #956; the backfill-parity test re-executes the marked SQL against the Rust twin), and recertification cancels open cycles in the create tx (3730:21-23). snap_periodic_reports = one row per (certification, due_month) with generation reserved for 5.2’s tombstones + the stored 3730 calendar + status machine whose transitions are single conditional UPDATEs (WHERE = legality; 409s name the current status; VCL ≥10 days CHECK-backed); "discrepancies" modeled as the typed vcl_reason/vcl_detail on the VCL transition rather than a distinct status. Six service-caller endpoints (list/get/form/vcl/verified/complete); legal dates server-stamped via a single legal_today() funnel (appeals B8 precedent — net B8 15→12 with the three query feeds + scheduler converted to tier-2 as_of seams). Scheduler materializes cycles (bounded, idempotent, missed calendars tombstoned calendar_elapsed_at_cutover) and gauges the three due-list predicates 5.2 will drain. Completion tx: cert row FOR UPDATE (cert→cycle lock order shared with supersede — deadlock-free), processed stamp + change-report rows (130%-FPL flag decided in the INSERT; the swallow-on-error mark_requires_redetermination deleted; three insert fns unified executor-generic) + typed renewal.snap_periodic_report_processed (mq-topology-allowlisted until the 5.2 consumer). Interim-contact handler made atomic. Store-level lifecycle + due-list + backfill-parity tests consolidated into one sequential DB test (global-scan materializer + shared devstack = parallel tests would steal batch slots); API tests cover the state machine, atomicity evidence (change rows + outbox in one commit), concurrent double-complete (one 200/one 409/one event), and completion-after-recert losing cleanly.

MR 5.2 (the two-notice calendar as the action pipeline)

15th-prior informational notice; 5th-of-month combined notice = THE adequate termination notice → creates the adverse action with that notice as its dispatched evidence version (NO duplicate NOA); closure by month-end SOP via the enact primitive; generation tombstones both directions (completion-before-trigger no-ops the late trigger); renewals terminal consumer stamps certification terminated_at; NO CB on these actions (P4); veto → worker re-determination queue (schema’d, authorized, idempotent) — no auto-retrigger.

Done (2026-07-21) — #1107: the drains ride the DAILY renewals scheduler (bounded, idempotent — the 15th trigger is a one-tx stage+status-flip; the combined trigger is an idempotent provenance-triple schedule call + cycle stamp, so a crash between them converges next tick). The action is the FIRST non-worker created_source (periodic_report/certification/generation via the new ActionSourceClaim); exempt-shaped per 3730:98-108 so cb_available=false falls out of the P4 snapshot (cb_rule cites 3730:37; appeals refuses CB unchanged); effective date = last workday ≤ month-end (weekend-only, appeals-precedent rationale — moving earlier only WIDENS the enact window due..EOM). The combined notice = dispatched evidence via the EXISTING evidence loop, discriminated to the pr-combined template by a new manifest created_source discriminator (status-mechanism parallel; worker actions keep the generic NOA); the 15th letter is deliberately NOT action-bound (would mint enact-gate evidence). Closure = the existing hourly enact sweep, unchanged. NEW pinned terminal events enrollment.adverse_action_{terminated,vetoed,cancelled} (provenance triple; staged in the enact/veto/cancel txs — enactment previously published NOTHING); renewals' canopy-renewals.adverse-actions consumer stamps cert (single terminated_at setter, now executor-generic) + cycle, or feeds pr_redeterminations (idempotent on the dead action; 2 endpoints; cancelled-with-reason periodic_report_completed is benign supersession and never queues). Tombstones both ways in enrollment’s periodic_report_completions (409 the late trigger; cancel the in-flight action), serialized on a per-(certification, generation) advisory lock against the schedule path. Deviations from the row as written: the combined notice is minted by the ACTION’s scheduled event (ordering (a) — action first), not by a renewals-side notice that then binds — the evidence machinery only supports action-first binding, and it makes the no-duplicate-NOA property structural; the VCL-failure closure arm (Chart 3730.1 row 2) needs its own notice-content decision → #1128. Also fixed in-MR: the #1106 broker-ACL gap for renewal.snap_periodic_report_processed. Cross-service in-module pipeline test (drains → routed letters → evidence → sweep → terminated → stamps; veto → queue + 404 double-action; completion → cancel + tombstone refusal). J-review deltas fixed pre-commit: the periodic_report source_reference is CANONICALIZED before locking/storing (a parseable-but-non-canonical UUID claim would have taken a different advisory lock and evaded the completion-cancel match); the periodic_report_completed reason is a pinned contracts constant; source: None omits from the wire (additive against pre-#1107 deny_unknown_fields). The advisory-lock INTERLEAVING itself has sequential-orderings coverage only — the concurrent race is MR 6.1’s interleaving matrix (completion-before-trigger is on its list).

MR 5.3 (reopen/proration per Chart 3730.1)

Verification-or-form within 30 days after the due month → reopen + prorate from receipt — a narrow scoped reopen path on the enrollment (the general restoration builder stays a successor, #1113). TWO SOPs per the chart (the row as originally written conflated them): verification cure (row 4) = 5 workdays from receipt; nonfiler late filing (row 5) = 30 days from receipt, expedited N/A. Calendar journey-grade tests.

Done (2026-07-21) — #1108: the reopen is ARM-DERIVED from the terminated row (vcl_sent_date → row-4 cure, else row-5 late filing with a complete form required per 3730:35 — arm-generic now so the #1128 VCL-failure termination arm plugs in), window + SOPs as cited policy keys ×2 rulesets. Converge-on-retry order: enrollment’s idempotent per-action reopen FIRST (narrow un-terminate: periodic_report source only, enacted only, terminated-by-this-action belt, no-live-successor fence, lifecycle_revision bump, append-only enrollment_reopens receipt + reopened signal; the action STAYS enacted — no status-vocabulary migration), then one renewals tx: cycle rejoins the NORMAL machine (terminated → verified|form_complete + reopened_date/sop_due_date; completion machinery unchanged) and reinstate_certification (only path away from terminated; clears terminated_at for the depth trend; the one-active-cert partial unique = the re-application fence). Proration = the EXISTING initial-month fn generalized (prorate_month_from) with the reopen receipt as a second anchor in issue_benefits; missed intervening months NOT restored (#1113). The #1107 enacted-arm operator alert goes quiet on a reopen receipt. Tests: store fence matrix, add_workdays proptest, pipeline phase D (reopen negatives → prorated issuance → completion leaves the enacted action untouched). Discovered: existing proration rounds nearest-cent and the under-$10 initial-allotment rule is unimplemented — R4 issue filed rather than silently changing money math in a reopen MR.

MR 6.1 (journeys + interleaving matrix)

Upheld journey (real action via UI, CB election, next-cycle enactment, async assessment polling); pipeline journey (schedule → dispatched NOA evidence → clock advance → sweep → terminated); periodic-report calendar journey (15th/5th/month-end); Playwright clock helper. The full review interleaving matrix distributed to phase tests (decision-before-grant, completion-before-trigger, reversal-with-residual, overlapping upheld appeals, grant-vs-sweep, issuance-vs-termination, allocation ties + concurrent correction, assessment-to-claim loss, pending-EBT recovery, stale notice acks, final-appeal continuation, oral-withdrawal reinstatement, mixed-version replicas, broker restart, pre-binding publication, malformed/wrong-source, multiply-stayed cancellation, zero-dollar concurrency, retry-budget exhaustion).

Done (2026-07-21) — #1109: matrix coverage was AUDITED first (six contextless readers over the phase suites; verdicts + the build list are the issue’s second comment) — already-covered items cited, not re-tested (overlapping upheld appeals, zero-dollar concurrency, oral-withdrawal reinstatement; broker restart is opt-in fault-injection by posture; reversal-with-residual = 3.2’s reversal_after_enactment signal path + the dead-action void/requeue tests added here; allocation ties are pinned pure + the concurrent-correction contest is the racing-full-applies test here; pre-binding publication stays the audited configuration posture — the capture-recovery leg is runbook territory). The genuinely-concurrent gaps landed as races: grant-vs-sweep, decision-before-grant, issuance-vs-termination (+ the untested fenced initial-issuance activation), multiply-stayed n-ary cancel, racing dispatch stamps (arrival-order versions + supersession + the gate’s latest-dispatched read; the documented 23505 redelivery retried in-test), and the 5.2-deferred completion-vs-trigger advisory-lock race. The zero-coverage 4.2 worker paths all gained tests (retry-budget exhaustion, apply_dead_action, ack re-emission, dead-action sweep against a REAL vetoed action, pending-EBT Defer→settle→commit, two FULL apply passes racing → billed-once). Journeys: NEW POST /v1/renewals/scheduler/run (advisory-locked on-demand daily pass; gated clock, no wire date; caller-gate + outcome-shape test) + the fleet-wide /test/clock Playwright helper (advance together, always reset; setClocksTo pins an epoch so the calendar journey is date-independent) enable the NEW pipeline journey (premature sweep REFUSES → dynamic advance past the CURRENT legal date → terminates on the NOTICED date; verified green under full profile + test-clock, 2026-07-21) and the NEW calendar journey (cohort via backdated START under the pinned epoch; 15th → 5th mints the termination → month-end sweep closes; verified green same run). Both #1123 journeys REBUILT action-bound (upheld: CB via the binding, continued month issued AFTER the election, clocks past the P2 cessation, ASYNC assessment polled; change-during-hearing: appeal #2 is a narrative grievance with provably NO CB) — but the rebuild EXPOSED #1136 (portal files the applicant membership as self; the #1097 HoH resolver accepts head only → worker enrollment creation refuses every portal-filed household), so both re-parked under test.fixme(#1136) — and went green with that fix (2026-07-22, 10.3s/9.9s under full profile + test-clock). The un-parking surfaced two more product gaps, both issue-tracked and honestly compensated in-spec: #1137 (the portal apply address is dropped at finalize, so the notices recipient gate blocks every portal-filed household — each journey gained an explicit worker addAddressViaUi beat) and #1138 (auto-enroll’s first issuance never settles — no EBT drive on the subscriber path — so the upheld journey’s contested arc moved a couple of days into the NEXT logical month, keeping the stuck-pending row pre-window for the month-granular Chart B2 window; the enrollment beat became ensureLiveEnrollmentViaUi because auto-enroll now legitimately races the worker’s create into the #1130 fence). Deviations: matrix items whose machinery is absent are issue-tracked, not faked — final-appeal continuation #1132, mixed-version parking #1131 (first ParkEvent caller wired by #1130), reopen-vs-reapplication fenced+raced in #1130; malformed-input nacks to DLQ by contract (parking is for recognized-but-unhandleable). recordDecisionViaUi was latently broken since #1099 (required date fields unfilled) — fixed here.

MR 6.2 (seeds + catalogue + sweeps)

Seeds (HoH model/generators; linked actions for synthetic appeals/termination notices; suspended seeds retained; ADR-033 background density); compliance action-catalogue bindings; doc-consistency + clock-consistency sweeps.

Done (2026-07-22) — #1110, built from three contextless surveys (the build list is the issue’s second comment). Seeds: HoH was already DONE (#1097) and suspended seeds intact — the real work was replacing the fabricated standalone termination notices with REAL scheduled adverse actions (FUTURE legal windows so the live enact sweep never eats seeded enrollments; every third cancelled as history) + dispatched version-1 evidence + #1101-bound notices rows; action-bound seeded appeals (not_applicable election — fresh granted_legacy only as unbound pending history, never decided/liable, killing the #1105 backfill scanner’s hourly FAILED spam on seeded rows); a GUARANTEED P11 cohort (counted over approved households — a raw-index slice left the cohort empty on real seeds) with midpoint cycles in three rotating kinds (processed / recert-cancelled / live-inert notice_sent); four new seed-verify ref checks + the core.enrollment-head-resolvable invariant. Catalogue: #799’s two rows flipped bound, five rows added (incl. the previously ROWLESS termination-execution mandate on the enact sweep), five re-pointed, gate clean 358/508. Clock: the last two ungated reads gated (enrollment summary year, renewals trend anchor); the structural gaps filed (#1140 web off-clock, #1141 dispatch-evidence wall-clock, #1134 annotated with the UTC-frame residue). Docs: 8 pages corrected (incl. services.adoc’s absent enrollment/renewals pipeline roles and the appeals 90→60-day fix) + both restructured walkthroughs rewritten from the verified specs. Deviations: per-service test-clock passthrough normalization noted in #1140, not done (cosmetic — the workspace flag covers the fleet); seed-verify’s pre-existing runtime-orphan noise filed as #1142.

MR 6.3 (UAT-milestone closeout)

Plan Status → Done; nav → Archive; UAT-labeled closing comment on #1084 enumerating the production-gap register with issue links; the final MR carries the auto-close keyword for #1084 (written out only there — never earlier, per the non-final-MR keyword rule).

Done (2026-07-22) — #1111: plan archived (file + nav entry to Archive per house convention); the production-gap register below now carries its tracked successor issues inline; #1115 (broker principals IF 0.7 descoped) closed moot — MR 0.7 shipped, the condition can never trigger; the UAT-milestone closing comment lives on #1084.

Policy rulings (P1–P13) — grounded in the policy cache

Sources: rulesets/georgia/.policy-cache/dfcs-snap/modules/snap/pages/ 3730.adoc (periodic reporting, eff. June 2026), appendix-b-hearings-overview.adoc, appendix-b-initial-hearings.adoc, appendix-b-final-appeals.adoc (eff. July 2024). Encoding per ADR-003/011 (keys + citations in BOTH rulesets).

  • P1 — CB election. Within 14 days of the notice date (Chart B2): timely notice → continue at prior level; adequate notice → reinstate within 5 working days; good cause after 14 days → reinstate on OSAH approval only; assume continuation unless waived (Form 118); repayment obligation disclosed and recorded. Replaces the generated column request_date < adverse_action_effective_date (which can never fire for adequate notices). New key continued_benefits_election_days = 14.

  • P2 — CB cessation is a typed event set with an actual cessation date: initial decision; certification end (reapply); federal-policy-only determination (immediate, no additional timely notice); mass change; unrelated eligibility change; timely final appeal extends continuation (Chart B3, 30-day window). Assessment windows end at the recorded cessation, never a formula.

  • P3 — Agency-favorable decisions: decrease in the next month’s issuance cycle after the decision is received, with adequate notice that must not advertise another hearing; claim established (Chart B1). decision_signed_datedecision_received_date; case action within 10 days of receipt.

  • P4 — CB availability by reason/context: periodic-report failure → NOT continuable (3730:37); recertification-time actions → NOT continuable (overview:60); worker adverse actions with timely notice → per P1. Per-reason in the reason-policy TOML with citations.

  • P5 — IPV/ABAWD are out of the v1 reason vocabulary (person-level sanctions; Chart B2 IPV row: never reinstate to pre-disqualification level).

  • P6 — Allocation: an issuance is billable once across all CB assessments; total order = (decision_signed_date, filing date, appeal_id); household-serialized full-set recomputation on corrections.

  • P7 — Post-cessation issuances: classified by actual issued_at against the cessation date, itemized; claimable per Chart B1 ("for any issuance, if appropriate").

  • P8 — Backdated exempt closures allowed with actor + authority audit; authority vocabulary from PAMMS 3705 adequate-notice categories.

  • P9 — Notice-safe reasons only; worker_initiated deleted — the worker vocabulary IS the 3705 category list (fixed factual codes with citations); exempt-immediate restricted by role. reason_detail never leaves the DB.

  • P10 — Periodic reporting is a calendar workflow (3730): initial notice the 15th of the prior month; combined reminder + adequate termination notice the 5th of the due month and that combined notice IS the termination notice (no further notice, no separate NOA); VCL ≥ 10 calendar days; closure by month-end (or last prior workday); the 30-day window after the due month is REOPEN/proration (Chart 3730.1), not pre-closure grace. No interim_contact_grace_days key exists.

  • P11 — Cohort gate: periodic reporting phased out for regular SNAP AUs effective 2026-03-02; legacy extended-cert cohorts continue until next recertification; Senior SNAP waived (3730:21-23, :64). snap_certifications.periodic_report_required + backfill migration.

  • P12 — Withdrawal is a lifecycle (overview:64-73): method, stage (pre/post OSAH submission), written confirmation within 10 days for oral pre-submission withdrawals + 10-day reinstatement window, finalization; only finalization releases stays. Dismissal carries a typed basis
    good-cause handling. withdrawn is not a decision enum value.

  • P13 — Decision clock = 60 days from request receipt, extendable by household-requested postponements (initial-hearings:53); 90 days is the filing window. Config migration decision_clock_days 90 → 60 + due-date recompute.

Design spine

  • adverse_action_id = the enrollment_pending_terminations row id, mandatory through notice → appeal filing → stays/links → events → assessment → claim. Reversal is a veto: durable, order-independent, moots every stay.

  • Versioned notices: the action’s legal dates derive from the latest DISPATCHED adverse_action_notices version; repair = successor version with new legal dates; evidence = render/upload success + provider- accepted dispatch, compared as jurisdiction-timezone DATES.

  • One guarded enact primitive for every path; gates on stays, notice evidence, the enrollment lifecycle_revision fence, and an under-lock date re-read. Global lock order: enrollment row → action rows ORDER BY id.

  • Synchronous fenced stay at filing — a stay receipt before the CB grant commits; pending_stay + retry when enrollment is unavailable.

  • Topology-first rollout — consumers/bindings deploy before producers activate, every phase (xtask lint from MR 0.3).

  • Money path: assessment work items (HTTP outside the inbox tx), fenced apply, household-serialized allocation, claim provenance
    append-only adjustments, acknowledgment + backfill scanners.

Production-gap register (the UAT close is labeled with these)

  1. Production notice delivery adapters (mail/SMS/email carriers with idempotency-key receipts) — the delivery outbox + dispatch evidence are delivered by MR 0.5; carriers are not built anywhere in this plan. Tracked: #1112.

  2. Full restoration/reinstatement builder (beyond MR 5.3’s periodic-report reopen). Tracked: #1113.

  3. Ledger-state automation for complex claim adjustments (repayment-plan recompute beyond append-only entries + ops flags). Tracked: #1114.

  4. Per-service broker principals IF MR 0.7 is descoped by decision — N/A: MR 0.7 SHIPPED (18 per-service principals + ACL tests); #1115 closed moot at the UAT milestone.

  5. Non-SNAP continued-benefits assessment. Tracked: #1116.

  6. Reduction-type + sanction-type adverse actions (#1002 remainder; the entity generalizes). Tracked: #1117.

Verification

  • Per MR: pre-push battery; focused integration with .ports.env; the phase test matrices named in the Status rows.

  • End-to-end at MR 6.1: the three journeys under --devstack-profile full with the test-clock feature build.

  • Mid-flight invariants: after Phase 0 the substrate holds rights-bearing flows; after Phase 2 filings are action-bound with real CB election; after Phase 3 nothing enacts without notice evidence; after Phase 4 assessments are allocation-safe and reconciled; after Phase 5 the periodic-report calendar is policy-true.

Edit this page · default