Plan: Scenario Inventory & Human-Fidelity E2E (epic &61)

On this page
NOTE

Implements ADR-031 §3 for epic &61 (parent &58), under the corpus architecture ratified by ADR-032: an engine corpus (synthetic test-min/test-max fixtures, universal + election-dependent scenarios, canopy CI) and per-jurisdiction conformance packs (Georgia first — the UAT suite and onboarding template). Consumes &60’s action catalogue (scenario steps reference catalogued actions; state-manual rows feed conformance scenarios) and &59’s currency assurance (conformance journeys assert against current values). Grounding below is code-verified (2026-06-09; ADR-032 amendments 2026-06-10). Issues are cut from the Status rows per ADR-013 once this plan lands.

Status

MR Description Status

MR1 (inventory schema + gate)

Scenario-inventory schema in canopy-policy: a ScenarioEntry = id, title, programs, life-events, regulatory citations, actor journey summary, complexity tier (unit — JDM fixture territory / flow — single-surface E2E / journey — multi-life-event), scope (universal | election-dependent) + elections keys per ADR-032 §1, and coverage bindings (e2e spec file + describe label, JDM fixture name, integration test path — any combination). Data at compliance/scenario-inventory/{program}.toml (cross-program scenarios in cross-program.toml); jurisdiction conformance bindings at rulesets/{jurisdiction}/scenarios/ (ADR-032 §3). Election keys resolve against the federal option registry (compliance/federal-options/{program}.toml — key, authorizing CFR cite, legal values; populated incrementally as scenarios reference options). New cargo xtask scenarios audit: validate schema, reject unknown election keys, verify every binding resolves (spec file exists + contains the bound describe label; fixture exists and is not one of the 2 known shells; test path exists), and report per-scenario status — covered / partial (bound but tier under-served, e.g. a journey scenario bound only to a unit fixture) / uncovered — reported per corpus (engine vs per-jurisdiction conformance). Exit 1 on dangling bindings (a lie in the inventory); uncovered scenarios are a report, allowlist-free — the count is the burndown metric. CI job adr-031-scenario-coverage, allow_failure: true until MR3’s triage.

Done (2026-06-10) — canopy_policy::scenario (schema incl. scope/elections/blocked_by + option registry + per-corpus evaluate; 11 unit tests incl. shell-rejection, tier-partial, scope/election consistency), cargo xtask scenarios audit (corpora: engine + conformance:{jurisdiction}), advisory CI job. Seeded end-to-end: 2 registry options, 3 engine rows (covered flow via e2e describe + integration test; uncovered election-dependent BBCE arm; covered unit via non-shell JDM fixture), 1 Georgia conformance row — live run: 3 covered / 1 uncovered, clean. Deviation: schema carries blocked_by (runnability-map issue refs) from v1 so MR2 needs no schema change.

MR2 (SNAP inventory)

Author the SNAP scenario inventory — the policy-reading deliverable. Enumerate from 7 CFR 273 + PAMMS: the 273.12 change-type space (income up/down, member add/remove, address, shelter/utility, dependent-care, child-support changes — recon: today only a free-string change_type exists and only income_change triggers redetermination, services/canopy-renewals/…​/change_reports.rs:38), expedited→regular transitions (273.2(i)), interim contacts, ABAWD clock edges (273.24: month-counting, exemptions, regaining), claims/recoupment (273.18), hearings + continued benefits (273.15), IPV/ADH paths, recert (273.14), churn, mixed/immigrant households (273.4). Tag every row universal or election-dependent (+ election keys) from the start per ADR-032 — no retrofit pass. Bind what today’s 43 specs / 19 fixtures / 24 personas actually cover (the recon mapping is the seed; per ADR-032 §3 those bindings are the Georgia conformance pack seed, since all are Georgia-seeded); leave the rest honestly uncovered. Mark runnability while binding: an uncovered scenario whose actions are unbound catalogue rows gets the blocking issue refs (#771-#848 families) — the runnability map is the endpoint-build prioritization. Expected outcome: a large uncovered count — that number IS the deliverable.

Done (2026-06-10) — 5-slice wave: 201 engine rows (204 with MR1 seeds) + 20 Georgia conformance rows (PAMMS-procedural, rulesets/georgia/scenarios/snap.toml) + 12 new registry options (14 total). Gate clean FIRST RUN (zero dangling bindings): 29 covered / 19 partial / 177 uncovered — the honest SNAP scenario-space measure. Runnability map live: 87 rows (80 engine + 7 Georgia-pack) carry blocked_by; top blockers by scenario count = #788 expedited/intake (15), #794 restoration (9), #790 ABAWD (9), #805 work-registration (9) — the derived endpoint-build order. 28 election-dependent rows await MR5’s fixtures.

MR3 (gap triage + remaining programs)

TANF / Medicaid (incl. ELE/TMA/EE15 cross-program rows) / CAPS / WIC inventories, same discipline as MR2 (scope-tagged, runnability-mapped). State-provision scenarios derived from the pinned state manuals (DECAL 13-week job-search grace, DPH category-anchored cert expirations, Pathways…) land in the Georgia conformance pack (rulesets/georgia/scenarios/), not the engine inventory — per ADR-032 §3. Then triage: file issues for the highest-value uncovered scenarios (UAT-aligned SNAP journey tier first), link under &61, and flip `adr-031-scenario-coverage’s dangling-binding check to blocking (coverage percentage stays a report).

Done (2026-06-10) — 4-program wave: TANF 84 / Medicaid 58 / CHIP 14 / CAPS 39 / WIC 49 / cross-program 13 engine rows + 111 Georgia conformance rows + 31 new registry options (45 total). Full inventory now 461 engine + 111 Georgia = 572 scenarios across all 5 programs; gate clean: 95 covered / 59 partial / 418 uncovered. TANF agent modeled the thin TANF federal floor (need standard, deprivation, sanction structure left to states per 42 USC 602) as registry option keys so test-min/test-max elect both arms — 100 election-dependent rows total. Runnability map: 343 blocked_by rows. Triage: 39 journey-tier scenarios (the MR4 harness work-list) filed as 6 issues #849-#854 (SNAP change/intake/cert/adverse high-priority; Medicaid+xp and TANF/CAPS/WIC medium). Blocking flip done: adr-031-scenario-coverage drops allow_failure — dangling bindings now fail CI; uncovered stays a report.

MR4 (journey harness pattern)

Depends on the generative-seed-harness plan (ADR-033) through its MR3 — do not start before the endpoint-driven given library + step-primitives exist; the work-list is #849-#854. Establish the multi-life-event journey pattern in the E2E harness: a journey-* spec family + Playwright project. A journey spec is stateful and sequential (one describe, ordered steps, each step asserting the world before acting), built from ADR-033 §6 step-primitives with §3 relational assertions — never persona constants or literal policy values; its "given" is constructed by the endpoint-driven setup helpers against generated data, and it must pass under any seed. Add the first journey: job loss → applicant reports change → expedited screening → worker verifies + determines → adverse action on prior case → appeal filed with continued benefits → hearing resolution → recert — touching applications, verification, eligibility, snap, appeals, renewals, notices through the real BFFs. Placement: gated journey project (not the pre-push default battery; the pre-push e2e budget is ~5-8 min and a journey is minutes on its own) + CI on the full pipeline.

In progress — slices 1–7 Done (2026-06-14): the journey- pattern + project + first seven journeys land (slices 4–5 build out the hearings family; slices 6–7 open the cross-program journeys — shared-fact report + ELE grant). New demo+full-gated journey Playwright project (testMatch globs all journey-.spec.ts, so each follow-up slice is a new file with no config change). Slice 1 (tests/e2e/specs/journey-snap-lifecycle.spec.ts): constructs an eligible certified SNAP case through the real lifecycle (endpoint-driven given library — not a demo persona), reports a substantial irregular windfall through the persons endpoint (new reusable reportIncomeChange given helper), and the worker re-determines through the BFF: the case loses eligibility (gross-income boundary crossed) and the determination lifecycle generates a Notice of Action (the spec asserts a notice row appears — the NOA pipeline fired — not the notice’s specific adverse-action type; a type-specific assertion is a tracked refinement for a follow-up slice). Assertions are relational/derived — the approved→denied flip is the oracle, income amounts are construction extremes (inputs, not asserted thresholds), so it holds under any jurisdiction’s values. Binds snap.change.substantial-lottery-winnings; scenarios audit flips it ✓ covered [Journey] (95→96 covered, the first journey-tier row covered). Proven green live; the windfall income row + DENIED re-determination verified in the DB. Slice 2 (tests/e2e/specs/journey-snap-recert-churn.spec.ts): the lapsed-certification churn journey, and the first whose oracle is a coverage transition rather than a determination flip. Constructs an eligible household with a certification backdated to an already-expired window (lapsed out of in-force coverage), the household reapplies through the real apply endpoint (new createReapplication given helper — a fresh application against the existing household + a re-determination over current facts), the worker re-determines through the BFF (APPROVED — cross-surface consistency), the approved reapplication is re-certified (new recertify given helper), and the NOA pipeline fires. The oracle is the derived coverage transition (in-force cert end date < today → ≥ today, a different cert id — back in coverage), compared only to today(). Binds snap.certification.closure-churn-reapply; scenarios audit flips it ✓ covered [Journey] (96→97 covered). Honest scope (spec header + binding comment): closure is not a system event (the determination reads no cert state; "lapse" is absence-of-coverage by date as the renewals overdue feed observes it), and the 7 CFR 273.14(b)(2) 30-day late-renewal-vs-new-application proration branch is unimplemented — so the journey covers the churn arc + the "reapplication = new initial application" leg, not a system-enforced boundary closure. Proven green live; the churn fingerprint (one household, two snap_certifications straddling today) verified in the DB. Slice 3 (tests/e2e/specs/journey-snap-shelter-cascade.spec.ts): the address-change shelter-cost cascade, and the first whose oracle is a benefit-amount monotonic change. Constructs an eligible certified size-3 SNAP household (earner head + two children, so the size≤2 minimum-benefit bump never applies), reports its existing rent through the real persons expense endpoint (new reportExpenseChange given helper — the expense-side sibling of reportIncomeChange), the worker determines through the BFF (APPROVED, an interior benefit), then the household moves to pricier housing (a shelter-cost increase reported as an added rent row) and the worker re-determines — the recomputed excess-shelter deduction is larger, so net income is lower and the allotment is higher. The oracle is the derived monotonic increase (benefit AFTER > benefit BEFORE, a relation between the two observed determinations, no dollar/threshold asserted); construction values are inputs calibrated to keep the case interior (approved both sides, off the floor + max-allotment ceiling, below the excess-shelter cap, shelter above the 50%-of-income threshold), the direction depending only on the federal jurisdiction-invariant deduction percentages. Binds snap.change.address-change-shelter-cascade; scenarios audit flips it ✓ covered [Journey] (97→98 covered). Honest scope (spec header + binding comment): the spec exercises the deduction recompute cascade, not the row’s "request verification → fails to verify → remove deduction" sub-flow (needs RFI/clarification machinery the system does not model — same gap as snap.change.unclear-information-clarification); shelter is the SUM of shelter-cost rows, so a move is modelled by adding a rent row (the expense endpoint adds, it does not edit). Proven green live; the move fingerprint (the constructed head holds two rent rows, $600 then $300, summing to $900) verified in the DB. Slice 4 (tests/e2e/specs/journey-snap-change-during-pending-hearing.spec.ts): an unrelated change acted on during a pending fair hearing — the first slice to construct a hearing through the real appeals service (opens the hearings family). Constructs an eligible certified size-3 SNAP household, the worker determines through the BFF (APPROVED, interior benefit), the household files a fair hearing on that determination through the real appeals endpoint (new fileAppeal given helper — a same-day filing against a future adverse-action effective date, so continued benefits are auto-granted per 7 CFR 273.15(k); "hearing pending"), then an unrelated shelter-cost increase is reported and the worker re-determines, and the household files a second hearing on the new action (against the post-change determination, read via the new latestDeterminationId helper). Three composed relational/derived oracles: (1) benefit-amount monotonic increase (the unrelated change was acted on); (2) derived-artifact count — the household’s appeal count rises by exactly one (listAppeals before/after — a second hearing request on the new action); (3) cross-service invariant — the first appeal, re-read after the re-determination (getAppealStatus), is still pending (acting on the unrelated change does not disturb the pending hearing; appeals/determinations isolated by service boundary, ADR-001). Binds snap.hearings.change-during-pending-hearing; scenarios audit flips it ✓ covered [Journey] (98→99 covered). Honest scope (spec header + binding comment): the "adverse action" framing is narrative (the appeals contract files against any determination id + effective date; it does not verify the contested determination reduced benefits — both appeals reference real signed determinations); "the hearing authority is notified of the changed posture" is not modelled (no link from a re-determination to a pending appeal) — the spec asserts the structural coexistence, not a notification. New helpers fileAppeal / listAppeals / getAppealStatus (given/appeals.ts, the first non-SNAP-lifecycle given module) + latestDeterminationId (given/snap.ts); CANOPY_TESTAPPEALS_URL added to the canopy-e2e compose env. Proven green live (13 passed); the hearings fingerprint (one household, two pending appeal_requests, continued benefits granted on both, against two distinct determination ids) verified in the DB. Slice 5 (tests/e2e/specs/journey-snap-upheld-decision-overpayment.spec.ts): carries the hearings family from "pending" through a decision to its financial consequence, spanning appeals → enrollment → the SNAP overpayment-claim ledger. Constructs a SNAP case, seeds the continued benefits being paid pending the hearing as real issued benefits via the canopy-enrollment service (new createEnrollment + issueBenefit helpers), files a BACKDATED appeal (continued benefits granted with a past start the issuances fall inside), and records an upheld_agency decision via the appeals service (new recordDecision helper). Two oracles — a new kind: a derived cross-service claim equality, and the first across an asynchronous event-driven boundary: (1) synchronous — the appeal’s assessed overpayment_amount equals the SUM of the issued continued benefits (getAppealOverpaymentCents vs the summed issueBenefit responses); (2) asynchronous — record_decision publishes appeal.overpayment_assessed, canopy-snap’s subscriber auto-opens an overpayment claim in its own DB (ADR-001, no callback), and the journey polls the SNAP ledger (new pollOverpaymentClaim helper) until the claim appears, asserting its claim_amount_cents equals the same summed issuances. Every figure is read back from a real prior step. Binds snap.hearings.upheld-decision-claims-continued-benefits (was partial — integration-bound only); scenarios audit flips it ✓ covered [Journey] (99→100 covered). Honest scope (spec header + binding comment): the benefit-decrease step is not modelled (issuances seeded directly); no BFF affordance records a decision (recorded via the appeals service); the appeal→claim link is by household + error_type (overpayment_claim_id never back-populated); the claim is eventually consistent (event-driven) so the oracle polls. New given/enrollment.ts + given/snap-claims.ts modules + a putJson HTTP helper; CANOPY_TESTENROLLMENT_URL/__SNAP_URL added to the canopy-e2e compose env. Proven green live (14 passed); the cross-service fingerprint (an auto-opened SNAP overpayment claim for the constructed household, claim_amount_cents=summed issuances, error_type=continued_benefits_on_appeal, status open) verified in the DB. Slice 6 (tests/e2e/specs/journey-snap-cross-program-report.spec.ts): a change reported through the TANF case counts as a SNAP report — the first slice to span two benefit programs (opens the cross-program journeys), introducing the first non-SNAP given-builder. Constructs a public-assistance household (no earned income + two children) approved for both SNAP (via SnapCaseBuilder) and TANF (via the new createTanfCase helper — a TANF application against the same household + an orchestrator determination over the same facts), reports one income windfall once through the persons endpoint, and the worker re-determines each program through the BFF (runDetermination is program-agnostic). The oracle is a relational cross-program flip: the single shared-fact change flips both the SNAP and the TANF determination approved→denied (the cross-program generalization of the slice-1 income flip — one change proven to drive two independent program services' signed determinations); the windfall is a construction extreme above every jurisdiction’s gross-income ceiling, so it holds under any jurisdiction’s values. Binds snap.change.pa-household-cross-program-report; scenarios audit flips it ✓ covered [Journey] (100→101 covered). Honest scope (spec header + binding comment): canopy realizes 7 CFR 273.12(f) structurally — the eligibility orchestrator fetches a household’s facts once and distributes the identical snapshot to every program service, so a change recorded once in canopy-persons is read by SNAP and TANF alike (no per-program fact silo to re-report into); the program-parameterized change-report endpoint (canopy-renewals) is a tracking artifact that neither mutates facts nor drives re-determination (cert-gated, no TANF-cert-create endpoint) so it is not the mechanism exercised; TANF deprivation is orchestrator-inferred (provisional); the "same increase/decrease timelines" clause is not separately asserted. New given/tanf.ts (createTanfCase, the first per-program builder beyond SNAP) + a shared programOutcome/DetermineResponse parser extracted to given/determine.ts (snapOutcomeprogramOutcome(resp,'snap')). No new service URL or compose env — the TANF leg reuses applicationsUrl+eligibilityUrl (orchestrator dispatches to canopy-tanf in-network). Proven green live (all 6 slices green, 15 passed); the cross-program fingerprint (the irregular $12,000 windfall in canopy-persons + the caseworker-filed {tanf} application in canopy-applications) verified in the DB. Slice 7 (tests/e2e/specs/journey-snap-ele-grant.spec.ts): a SNAP approval grants the children Medicaid via Express Lane — the second cross-program journey (the event-driven grant, vs slice 6’s shared-fact report). Construct a family (no earned income + two children under the ELE child age gate) → SNAP approved → record ELE consent through the real canopy-applications endpoint (new recordEleConsent helper, given/ele.ts) → worker re-determines SNAP through the BFF → the approval event (consent now on file) drives the canopy-medicaid ELE subscriber to grant each eligible child a Medicaid-tier flag via ele-grant-2026, with no separate Medicaid determination. Relational oracle: (1) the grant surfaces on the worker case detail (poll the identity-hero badge); (2) the badge’s child count equals the eligible children constructed. Binds xp.ele.partner-approval-grants-medicaid (was partial — non-journey threaded-demo binding) → 101→102 covered. Honest scope: the grant is gated on consent already on file at approval time with no re-evaluation sweep (so consent is recorded before the triggering re-determination — the supported consent-then-approval order); ELE as configured in the demo (Georgia) jurisdiction; event-driven so the oracle polls. New postNoContent HTTP helper (the consent endpoint returns 202 empty). No new service URL/compose env. Proven green live (all 7 slices, 16 passed); DB fingerprint = two ele_status rows (current_status=active, granting_program_history={snap}, child DOBs matching the roster, expires_at ≈ today+12mo) in the canopy-medicaid program DB. Design deviation (ADR-013): the planned single 8-step journey (job loss → expedited → adverse action → appeal → recert) is delivered tracer-first — slice 1 establishes the full pattern (project, given-construction, relational assertions, scenario binding, demo gating) on the income-windfall→adverse-action spine, which is fully runnable today; the expedited/appeal/recert steps (whose endpoints are blocked_by #788/#793/#794 and need given-library extensions for change-reporting/appeals/recert) extend it in follow-up slices, each binding more #849-854 rows.

MR5 (synthetic engine fixtures — supersedes the single-testland design per ADR-032 §2/§4)

The adversarial pair: rulesets/test-min/ (smallest legal deployment — minimal program subset exercising ADR-005 degradation, strictest/declined elections: no BBCE, standard reporting, interviews required) and rulesets/test-max/ (all programs, all optional surfaces, most permissive elections). Round-number policy values (goldens auditable by inspection, immune to indexing churn); citations per ADR-032: each election cites its federal-option-registry entry (authorizing CFR provision), each value cites this plan’s fixture-design appendix via a fixture citation source kind that policy audit accepts only under rulesets/test-. Full per-jurisdiction artifact set per fixture (the recon-globbed 34-file shape: jurisdiction.toml, citations, composition x5, workflows, notices manifest or default-fallback). Parameterize the seed/e2e path: xtask e2e currently hardcodes jurisdiction: "georgia" (xtask/src/cmd/e2e.rs:183) — add --jurisdiction; run a smoke subset (sign-in, dashboard, one determination, one notice) against each fixture in a scheduled CI job, with at least one asserted test-min-vs-test-max behavioral divergence per elected option arm (proves the elections actually flow). Existing rulesets/default/ stays what it is (a georgia copy for operator bootstrap, stage-6 #499) — the fixtures are for *testing the option space, not bootstrap.

Done (2026-08-27) — the #761 MR (both SHAs in the issue’s closing receipt). Fixtures authored + adversarially verified (test-min 125/125, test-max 246/246 citations; audit clean over all five families); citation kinds (fixture, option_registry) audit-enforced; divergence assertions battery-gated in-crate (snap self-employment election, renewals BBCE gross screen, rules corpus bootability); one-export CANOPY_JURISDICTION boot seam + xtask e2e --jurisdiction; notices default-template fallback (#1265-doctrine-gated); the relational fixture-smoke.spec.ts green against georgia pre-merge and wired to the schedule-gated fixture-smoke CI lane (FIXTURE_SMOKE=true schedule; first green fixture run pending the schedule post-merge, shadowed by the #1397 runner-disk condition). Deviations + rules: the fixture-design appendix. Pre-existing debt surfaced: #1613 (federal alien-eligibility JDM carries a georgia-prefixed name).

MR6 (integration-test jurisdiction hygiene)

Burn down the hardcoded jurisdiction: "georgia" literals in integration-test files (23/19 at recon; 50/20 by landing — the codebase grew) to a single canopy-test-lib helper (test_jurisdiction(): CANOPY_TEST__JURISDICTION, default georgia, &'static str via OnceLock so &str fields and .to_owned() sites swap mechanically) so service tests can run against the test-min/test-max fixtures without a sweep. Deliberate keeps, each waiver-commented: the contracts-crate serde roundtrip fixtures (the value never dials a service; keeps the heavy test-lib dev-dep out of a contracts crate) and canopy-web’s self-contained composition harness (its literals pair with LOCAL registry/cache-key fixtures). MR5 seam notes found by this MR’s review: insta snapshots pin georgia-derived ruleset_version values (caps/wic snaps, alien_eligibility_test), so the fixture run needs per-jurisdiction snapshot handling, and the tests' loaded params must match the jurisdiction the LIVE stack booted with — the override is stack-wide, not per-test.

Done (2026-08-27) — the #762 MR (both SHAs in the issue’s closing receipt); 50 literals swept across 20 files, swept-crate suites green on the default; the override var is the MR5 seam.

MR7 (journey walkthrough pairing gate + UI-gap backlog — #972, ADR-031 Amendment 1)

Realize the epic-&61 human-fidelity half: every covered journey must ship a human-followable Antora walkthrough paired with its journey- spec, or carry an issue-backed block. Adds a walkthrough binding kind + walkthrough_blocked_by field + MissingWalkthrough finding + a reverse audit (OrphanSpec: no unbound journey-.spec.ts) + a screenshot-existence check to cargo xtask scenarios audit (canopy_policy::scenario). New walkthroughs/ docs module + nav-linked coverage landing page. Finding (the deliverable’s honest core): all 9 existing SNAP journeys are UI-blocked — none is hand-followable, because worker/applicant-portal UI (cert-create, SNAP appeal file/decision, enrollment/issuance, ELE-consent) and multi-program intake and backdating do not exist. So this MR ships the gate + the UI-gap backlog (#973–#981, each a Sept-2026 human-UAT blocker) rather than fig-leaf walkthroughs; each journey is walkthrough_blocked_by its gap issue, and walkthroughs land as the gaps close (an acceptance criterion on each gap issue removes the marker + adds the walkthrough). Also binds the 2 previously-orphan journey specs (income-materialitysnap.change.income-exceeds-130pct-mid-period, overpayment-recomputesnap.integrity.claim-calculation-lookback, promoted unit→journey) and files a fix: (#982) for stale worker creds in the testing guide. #852’s spec is unauthorable pending a real feature (#981: ADH IPV-not-established → non-fraud claim reclassification).

Done (2026-07-05) — !<mr> ; gate + 22 scenario.rs unit tests; scenarios audit clean (103 covered / 57 partial / 412 uncovered). Follow-on: #850 (candidate hand-followable intake journey) and the walkthroughs themselves land as #973–#981 close.

Design — grounded current state (code-verified)

  • E2E harness: Playwright projects (tests/e2e/playwright.config.ts); auth-setup chains 9 users; on-demand visual-baseline projects (vb-*) key off CANOPY_E2E_VISUAL_BASELINE (set by xtask e2e --visual); full-stack projects (journey, worker-determination-ele) off CANOPY_E2E_DEVSTACK_PROFILE=full. The longest specs prove dual-context (:8090 applicant + :8080 worker) and cross-service-event journeys already work.

  • Coverage today is default-seed-driven: the generative seeder’s random bulk + phase1b scenario-targeted households + the phase14_cast login-capable applicants (the demo dataset was retired into the default seed in #716); 19 JDM math fixtures (2 shells: snap-eligibility, medicaid-non-magicrates/canopy-rules-client/tests/ruleset_happy_path_test.rs:52-53); the spec suite. No artifact links any of these to the scenario space; JUnit output lists spec names, not scenarios.

  • Change-reporting surface: endpoints exist (POST …​/change-report + program-parameterized variants, canopy-renewals/src/api/mod.rs:545,677) but change_type is a free string and only income_change marks requires_redetermination — most of the 273.12 space is recordable but not actionable; the inventory makes that visible per-type.

  • Jurisdiction: threaded per-service at startup via CANOPY_*__JURISDICTION (single-tenant per ADR-006); seed/e2e hardcode georgia; rulesets/default/ is a verbatim georgia copy; the test literals (23/19 at recon, 46/19 by MR6’s landing) now route through canopy_test_lib::test_jurisdiction() (MR6, #762). The per-jurisdiction artifact set is known (georgia: 34 files — jurisdiction.toml, citations.toml, theme, idp, composition x5, workflows x9, notices tree, param JSONs).

Design — decisions

  • The inventory tracks scenarios, not personas. Personas are seeded instances; a scenario is the policy-derived situation class. A scenario row may bind to a persona (via the demo seed) as its fixture, but the inventory axis is the CFR/PAMMS-derived space, so "what’s missing" is measured against policy, not against what we happened to stage.

  • Three complexity tiers with tier-appropriate coverage: unit scenarios are satisfied by JDM fixtures (cheap, exhaustive math edges); flow by existing-style specs; journey only by multi-life-event specs. The gate’s "partial" status (journey bound only to a fixture) prevents tier-laundering.

  • Uncovered is a report, dangling is an error. A binding that doesn’t resolve is a lie and fails CI; an honest gap is the burndown metric (mirrors quality-budgets philosophy — debt visible and monotonically shrinking, not hidden).

  • Journeys are full-stack-gated, not pre-push. The pre-push battery stays fast; journeys run on the full devstack in CI and locally on demand (cargo xtask e2e --devstack-profile full — --project journey).

  • Two corpora, one schema (ADR-032). The engine corpus (universal + election-dependent scenarios against synthetic test-min/test-max) answers "does canopy implement the federal option space?" and runs in canopy CI; conformance packs (rulesets/{jurisdiction}/scenarios/) answer "does this configured deployment behave per its policy?" and are each jurisdiction’s ship gate — Georgia’s pack is the UAT suite and the onboarding template. Georgia policy is deliberately NOT the engine corpus: un-elected option arms would go permanently untested, and real-value churn would rot the goldens.

  • Synthetic pair, not a second real state (supersedes the earlier single-testland sketch). Adversarially-elected test-min/test-max fixtures exercise both arms of every referenced election (a single fixture cannot); round numbers + federally-cited elections keep ADR-011 discipline without importing a second state’s policy-reading cost. A real jurisdiction onboarding later inherits the conformance-pack path instead.

Verification

  • Gate unit tests (fixture inventory + fixture spec tree: resolving binding, dangling binding → error, tier-mismatch → partial; shell-fixture binding rejected).

  • MR4’s journey runs green in the demo pipeline; screenshots + the journey’s step assertions reviewed against the design renders where UI is touched (project screenshot-verify convention).

  • MR5: test-min + test-max smokes green in their scheduled job; at least one asserted test-min-vs-test-max behavioral divergence per elected option arm (proves the elections actually flow); policy audit green over both fixtures (election keys cite the option registry, values cite the fixture-design appendix, fixture source kind rejected outside rulesets/test-*).

  • Each MR through the standard gate (validate + D1-D8 + force-merge squash=false).

Appendix — fixture design (MR5, #761; ADR-032 §2/§4)

The design authority every authority = "fixture" citation in rulesets/test-min/citations.toml and rulesets/test-max/citations.toml references (source_ref = "plans/scenario-inventory-e2e.adoc#fixture-design"). The VALUES themselves live in the fixture files (the audit’s ValueMismatch check keeps citation and file in lockstep); this appendix records the rules that produced them and the election matrix.

Personalities

  • test-min — the smallest legal deployment: enabled_programs = ["snap"] only (its jurisdiction.toml carries ONLY the snap + shared/jurisdiction/ notices/appeals sections — the ADR-005 degradation arm made concrete); every federal option DECLINED where law permits (no BBCE ⇒ live asset test, no simplified reporting, no heat-and-eat, no TSNAP, no ABAWD waiver, actual-cost self-employment, SUA mandatory, dependent care capped).

  • test-max — every program enabled, every optional surface on, most permissive elections (BBCE at the 200% lawful ceiling with the asset test eliminated, simplified reporting, statewide ABAWD waiver, standard self-employment percentage, MAGI adult expansion adopted).

Value rules

  1. Round numbers only — auditable by inspection, immune to indexing churn (money in whole hundreds of cents; percentages in fives/tens; day counts from {5,7,10,14,15,30,45,60,90,180,270}; months from {3,4,6,12,24,36,48,60}).

  2. Federal floors/ceilings are inviolable and identical in BOTH fixtures (expedited 7 days, expedited screening $150/$100, ABAWD 80h/3-in-36, IPV 12/24 months, CHIP floor 134% FPL, TANF federal 60-month limit, WPR 50/90, two-parent 35/55 hours, core-hours 20, processing SOPs 30/7/30/45/45 days, appeal window 90 + decision clock 60 days; the SNAP minimum benefit 2400¢ — 8% of the one-person max allotment, 7 CFR 273.10(e)(2)(ii)©, a floor the adversarial review caught the first draft violating; BBCE gross spans exactly the lawful 130→200 range across the pair).

  3. Wherever an election has an engine or service realization, the pair MUST differ on it — that difference is what the divergence assertions consume.

  4. Identity strings are obviously synthetic (Test-Min/Test-Max Fixture Agency, 1-555-0100, Testville, fips 00/99, UTC; holidays are six round dates as OBSERVED weekday shifts — the workday calendar refuses weekend entries at load); [policy_source] type = "manual", no repos (fixtures have no PAMMS).

  5. Structure-preserving tables: money/limit arrays keep georgia’s LENGTH and monotonic shape with round values; the adverse-action reason vocabulary and the TANF work-activity vocabulary copy georgia verbatim (product surface, not policy values).

  6. Federal-file-mirrored keys keep the shared federal files' EXACT values — the snap param-set loader refuses cross-source divergence (#1467 C5; single-sourcing is #1478): minimum_benefit_amount_cents 2400, homeless_shelter_deduction_monthly_cents 19900 (both caught live by the divergence tests, not the citation audit).

  7. Copied JDM files carry the FIXTURE’s jurisdiction prefix in their embedded name field where georgia’s carried georgia- (services look up {jurisdiction}-<ruleset>); the federal alien-eligibility file’s wrongly georgia-prefixed name is pre-existing debt, filed as #1613.

Election matrix (compliance/federal-options/ ↔ fixture arms)

Registry option Realizing jurisdiction key(s) test-min arm test-max arm Realization

snap.bbce

snap.bbce_enabled, snap.bbce_gross_income_limit_pct_fpl, snap.bbce_elderly_disabled_gross_pct_fpl, snap.bbce_asset_test_eliminated

not-elected

elected

engine/service

snap.simplified-reporting

snap.simplified_reporting, snap.periodic_reporting.*

not-elected

elected

engine/service

snap.esap

snap.certification_period_senior_months

not-elected

elected

registry-only

snap.transitional-benefits

snap.transitional_benefits_enabled

not-elected

elected

registry-only

snap.abawd-geographic-waiver

snap.abawd_waiver_active, snap.abawd_waiver_areas

no-waiver

waiver-in-effect

registry-only

snap.abawd-discretionary-exemptions

snap.abawd.discretionary_exemption_pct

not-utilized

utilized

registry-only

snap.comparable-disqualification

none (registry-only)

not-elected

elected

registry-only

snap.self-employment-expense-method

snap.self_employment.standard_deduction_enabled, snap.self_employment.standard_deduction_pct, snap.self_employment.boarder_income_deduction_method

actual-costs

standard-percentage

engine/service

snap.drug-felony-policy

snap.disqualifications.drug_felony_policy, snap.disqualifications.drug_treatment_exemption

full-ban

opt-out

registry-only

snap.sua-methodology

snap.sua.allow_actual_utility_costs

mandatory-sua

household-choice

registry-only

snap.child-support-treatment

none (registry-only)

deduction

income-exclusion

registry-only

snap.vehicle-exclusion-methodology

snap.vehicle_fair_market_value_excluded

snap-standard

tanf-rule-substitution

registry-only

snap.group-hearings

none (registry-only)

not-elected

elected

registry-only

snap.claims-establishment-threshold

snap.overpayment.minimum_claim_cents

default-125

approved-plan-threshold

registry-only

tanf.lifetime-limit

tanf.time_limit_months, tanf.federal_time_limit_months

not-applicable (program not enabled)

federal-60-months

engine/service

tanf.hardship-extension

tanf.hardship_waiver_enabled

not-applicable (program not enabled)

elected

registry-only

tanf.family-violence-option

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

tanf.work-sanction-structure

tanf.sanctions.*

not-applicable (program not enabled)

graduated-partial-then-full-family

registry-only

tanf.infant-exemption

tanf.wpr.infant_exemption_months_max

not-applicable (program not enabled)

elected

engine/service

tanf.ivd-sanction-scope

none (registry-only)

not-applicable (program not enabled)

reduce-at-least-25pct

registry-only

tanf.drug-felony-policy

none (registry-only)

not-applicable (program not enabled)

opt-out

registry-only

tanf.individual-responsibility-plan

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

tanf.deprivation-eligibility

none (registry-only)

not-applicable (program not enabled)

required

registry-only

tanf.assistance-unit-composition

none (registry-only)

not-applicable (program not enabled)

mandatory-standard-filing-unit

registry-only

tanf.need-standard-design

tanf.income_test_basis, tanf.gross_income_ceiling_pct_of_son, tanf.financial_standards.*

not-applicable (program not enabled)

gross-ceiling-and-standard-of-need

engine/service

tanf.earned-income-disregard

tanf.earned_income.disregard_type, tanf.earned_income.disregard_amount_cents

not-applicable (program not enabled)

standard-work-deduction

engine/service

tanf.diversion-program

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

tanf.pregnant-individual-coverage

none (registry-only)

not-applicable (program not enabled)

covered

registry-only

medicaid.medically-needy

medicaid.mnil_income_limit_by_bg_size, medicaid.mnil_income_limit_each_additional, medicaid.abd_mnil_individual, medicaid.abd_mnil_couple

not-applicable (program not enabled)

elected

engine/service

medicaid.institutional-income-cap

none (registry-only)

not-applicable (program not enabled)

special-income-level-elected

registry-only

medicaid.express-lane-eligibility

shared.timing.ele_renewal_sweep_window_days

not-applicable (program not enabled)

elected

registry-only

medicaid.section-1115-demonstration

medicaid.pathways_enabled, medicaid.pathways_income_limit_pct_fpl, medicaid.pathways_work_hours_per_month, medicaid.pathways_min_age, medicaid.pathways_max_age

not-applicable (program not enabled)

no-demonstration

engine/service

medicaid.extended-postpartum-12-months

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

chip.waiting-period

none (registry-only)

not-applicable (program not enabled)

none

registry-only

chip.premiums

medicaid.peachcare_premiums.tiers, medicaid.peachcare_premiums.exempt_under_age, medicaid.peachcare_premiums.exempt_foster_care, medicaid.peachcare_premiums.exempt_american_indian

not-applicable (program not enabled)

imposed-per-public-schedule

engine/service

caps.initial-income-threshold

caps.income_limit_initial_pct_smi

not-applicable (program not enabled)

85-pct-smi-maximum

engine/service

caps.graduated-phase-out-threshold

caps.income_limit_continued_pct_smi

not-applicable (program not enabled)

85-pct-smi

engine/service

caps.job-search-continuation-period

none (registry-only)

not-applicable (program not enabled)

extended

registry-only

caps.copayment-waiver

none (registry-only)

not-applicable (program not enabled)

waivers-elected

registry-only

caps.presumptive-eligibility

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

wic.breastfeeding-cert-one-year

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

wic.infant-cert-to-first-birthday

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

wic.child-annual-certification

none (registry-only)

not-applicable (program not enabled)

elected

registry-only

wic.processing-standard-15-day-extension

none (registry-only)

not-applicable (program not enabled)

extension-permitted

registry-only

wic.food-delivery-system

none (registry-only)

not-applicable (program not enabled)

retail

registry-only

not-applicable (program not enabled) rows are the program-subset divergence itself: test-min proves the stack RUNS without those services (ADR-005), which is the elected-arm assertion for every option of a program test-min omits.

Divergence assertions (the "elections actually flow" proof)

Scoped to options with a live realization on BOTH sides of the pair — narrower than the MR row’s original "per elected option arm" phrasing, and deliberately so (a registry-only option has no realization to assert; recorded here per ADR-013 living-spec):

  1. snap.self-employment-expense-method — the engine-registered SNAP divergence: SnapParameterSets::load (the REAL boot loader) yields standard_deduction_enabled false/true and the elected 50% only under test-max (in-crate canopy-snap test fixture_pair_diverges_on_the_self_employment_election — battery-gated). These values feed se_deduction and the snap-self-employment-deduction JDM.

  2. snap.bbce — runtime realization is the renewals gross screen: RenewalParams::load yields gross_income_limit_pct 130 (no BBCE, federal base) vs 200 (the lawful BBCE ceiling) — in-crate canopy-renewals test fixture_pair_diverges_on_the_bbce_gross_screen. (The engine’s asset-test thresholds come from the SHARED federal deductions file, not jurisdiction.toml — the loader REFUSES divergence on federal-mirrored keys, see value rule 6 — so the bbce booleans' remaining realization is the seed/scenario layer, as the matrix records.)

  3. Corpus bootability — NamedFilesystemLoader::scan over [federal, test-*] scans clean and registers each fixture’s OWN jurisdiction-prefixed SNAP eligibility ruleset (in-crate canopy-rules test fixture_corpora_scan_clean_like_a_service_boot): exactly the load the rules service performs at boot.

  4. Program-subset options — existential: the scheduled fixture-smoke lane boots test-min snap-only and test-max full and both smoke green (sign-in, dashboard, one determination, one notice).

Boot seams this MR added

  • docker-compose.yml: every jurisdiction consumer reads ${CANOPY_JURISDICTION:-georgia} (one export switches the stack).

  • cargo xtask e2e --jurisdiction parameterizes the SEED (default georgia; the bare battery is byte-identical).

  • canopy-notices falls back to rulesets/default/notices templates when a jurisdiction ships none — fail-closed outside CANOPY_ENV=dev behind CANOPY_NOTICES__ALLOW_DEFAULT_TEMPLATE_FALLBACK (#1265 doctrine); the fixtures deliberately ship no template tree.

  • Citation kinds: fixture (this appendix; staleness-exempt; rejected outside rulesets/test-*) and option_registry (source_ref must be a live compliance/federal-options/ key) — both audit-enforced.

Edit this page · default