Plan: Medicaid Resource/Medical Aggregation (#856, epic &63)
On this page
Implements both halves of #856 under epic &63 (medical 2026-06-16, resources 2026-07-18), governed by ADR-034 (the orchestrator builds each program’s complete, typed determine input; per-input resolution lands in the program handler from the threaded facts, the ADR-002 black-box owning its own policy). Follows the slice-1 precedent (#619 resolve_applicant_age) and the SNAP medical-deduction precedent. Builds on the frequency-normalization foundation (#861).
Context
The Medicaid /v1/determine handler read ctx.medical_expenses_monthly.unwrap_or(ZERO) and
ctx.countable_resources.unwrap_or(ZERO), but the orchestrator never sets those scalars — it threads
the raw ctx.expenses / ctx.assets arrays. So on the orchestrator path the medically-needy
spenddown saw $0 medical expenses and resource-tested COAs saw $0 resources. Investigation split
#856 into two halves of different readiness: the medical half landed first (2026-06-16); the
resource half followed (2026-07-18) once the conservative category projection resolved the
false-denial blocker that had deferred it (see Status + Design).
Status
| Step | Description | Status |
|---|---|---|
1. Medical-expense aggregation helper |
|
Done (2026-06-16) — 4 unit tests (incl. a persons-wire-shape |
2. Document the recognized |
|
Done (2026-06-16). |
3. Input-requirements manifest |
|
Done (2026-06-16). |
4. |
|
Done (2026-07-18) — 5 unit tests + a proptest invariant block (bounded-by-naive-sum, excluded-category no-op, exact bank-account additivity, override-wins) + 2 integration tests (QMB resource-over-limit denial from |
Design — decisions
-
Aggregate in the handler, not the orchestrator or JDM (ADR-002/003). The project pattern is Rust-aggregates-then-JDM-evaluates: SNAP sums assets + filters medical expenses in its handler (
canopy-snap/src/determine.rs), the Medicaid handler already sums income in-handler, and the MN-spenddown ruleset consumes only the pre-aggregatedinput.medical_expenses_monthlyscalar. Resource-exclusion rules are Medicaid policy (ADR-002 black-box) — placing them in the orchestrator would force it to know every program’s resource-counting rules. The eventual ADR-034map_medicaid_contextrefactor consumes the same pure logic. -
Override channel preserved.
ctx.medical_expenses_monthly(andctx.countable_resources) stay as override channels for direct callers / fixtures — the same idiom asctx.agein slice 1. -
The resource half counts conservatively, never naïvely. A naïve sum would count primary homes (no homeplace metadata) → false denials. The shipped projection (Step 4) inverts the failure mode: a category is summed only when no statutory exclusion could apply to it as a class, so a category-classification error can only under-count — a false-approval risk no worse than the pre-#856
$0default, and a strict improvement on it (over-limit liquid resources now correctly deny). Residual: a countedbank_accountmay itself hold statutorily-excluded funds the wire cannot flag (burial designation 20 CFR 416.1231, retroactive SSI/RSDI + EITC 416.1233/.1235) — within-account exclusions stay with the per-asset metadata under #778, and the override scalar is the worker’s correction path meanwhile. ADR-034 Decision 3 is satisfied:$0now means "no countable asset facts", not "input silently defaulted". The full SSI methodology stays with the per-asset metadata it actually needs (#778).
Verification
-
cargo nextest run -p canopy-medicaid— the 4 unit helper tests + the integration flip test. -
cargo run -p xtask — policy input-coverage—medical_expenses_monthlynow satisfied (noGapIssueOnNonGap); 0 findings. -
cargo xtask api-docs --update(aftercargo xtask dev refresh) — regeneratedocs/modules/ROOT/openapi/medicaid.jsonfor theExpenseItemrustdoc; thevalidateapi-docsdrift gate is then clean. -
Full pre-push gate:
cargo xtask validate+cargo xtask seed+cargo xtask e2e.