Plan: ADR-003 Compliance Remediation

On this page

Status

Step Description Status

1

Add missing parameters to jurisdiction.toml and federal JSON

Done (2026-04-06)

2

Extract FPL table from income_threshold.rs → fpl-2026.json

Done (2026-04-06)

3

Parameterize expedited screening thresholds in canopy-applications

Done (2026-04-06)

4

Parameterize certification period timing in canopy-renewals

Done (2026-04-06)

5

Parameterize renewal scheduler lookahead in canopy-renewals

Done (2026-04-06)

6

Parameterize IPV penalty schedule in canopy-appeals

Done (2026-04-06)

7

Parameterize ADH notice and issuance deadlines in canopy-appeals and canopy-enrollment

Done (2026-04-06)

8

Parameterize certification/renewal periods in canopy-snap determine.rs

Done (2026-04-06)

9

Update tests to use parameterized values

Done (2026-04-06)

10

Verify full test battery passes

Done (2026-04-06)

Issues: #298 (FPL JSON loading)
Branch: feat/save-adapter-endpoints (bundled with current work)

Context

ADR-003 mandates that all eligibility logic and federal regulation values live in versioned JDM rulesets or jurisdiction.toml, not in Rust code. This enables policy changes (annual FPL updates, legislative changes like HR1, FNS guidance memos) to be deployed as data changes without recompiling the application.

A full-codebase audit identified 8 files across 4 services containing hardcoded federal regulation values:

  • canopy-renewals/src/income_threshold.rs — Full 2026 FPL table inline

  • canopy-applications/src/expedited.rs — $150/$100 expedited thresholds

  • canopy-renewals/src/certification.rs — 22-month, 6-month, 75-day, 30-day timing

  • canopy-renewals/src/scheduler.rs — 75-day renewal lookahead literal

  • canopy-appeals/src/ipv/penalties.rs — 12/24/permanent penalty schedule

  • canopy-appeals/src/ipv/workflow.rs — 30-day ADH notice requirement

  • canopy-enrollment/src/issuance.rs — 7-day, 30-day, 365-day deadlines

  • canopy-snap/src/determine.rs — 6/5 month certification/renewal periods

The following were verified as already compliant:

  • canopy-snap/src/deductions.rs — loads from DeductionParams (jurisdiction.toml)

  • canopy-snap/src/abawd.rs — loads ABAWD params from jurisdiction.toml

  • canopy-snap/src/categorical.rs — loads BbceConfig from jurisdiction.toml

  • canopy-snap/src/determine.rs income classification — data taxonomy, not regulation

  • Frequency conversions (52 weeks/year, 12 months/year) — mathematical identities

Scope

In scope:

  • Add new parameter sections to rulesets/georgia/jurisdiction.toml

  • Refactor 8 files to accept parameters instead of using hardcoded literals

  • Create parameter structs and loading logic where needed

  • Update all affected tests to pass parameters explicitly

Out of scope:

  • Moving expedited screening to a JDM ruleset (future: full rules-engine evaluation)

  • Alaska/Hawaii FPL tables (tracked separately)

  • Creating new JDM rulesets for IPV penalties (simple enough for jurisdiction.toml)

Design

The pattern is consistent across all 8 files: replace hardcoded literals with parameters loaded from jurisdiction.toml at service startup.

Parameter Loading Pattern

Each service already loads jurisdiction.toml via its params.rs module (or will add one). The pattern:

  1. Add TOML keys to rulesets/georgia/jurisdiction.toml

  2. Add corresponding fields to the service’s parameter struct

  3. Pass params into functions that currently use literals

  4. Tests construct params explicitly (no file I/O in unit tests)

New jurisdiction.toml Sections

[snap.expedited]
low_income_limit_cents = 15000         # 7 CFR 273.2(i)(1) — $150/month
liquid_assets_limit_cents = 10000      # 7 CFR 273.2(i)(1) — $100

[snap.certification]
elderly_disabled_threshold_months = 22 # Inferred from 24-month cert period
interim_contact_months = 6             # 7 CFR 273.12(a)(1)(ii)
renewal_notice_advance_days = 75       # State policy
second_renewal_notice_advance_days = 30

[snap.issuance]
expedited_days = 7                     # 7 CFR 273.2(i)
standard_days = 30                     # 7 CFR 274.2(b)
expungement_days = 365                 # 7 USC §2016(h)(9)

[snap.determination]
standard_certification_months = 6      # Certification period for approved cases
standard_renewal_months = 5            # Renewal notice offset (cert - 1 month)

[snap.ipv]
first_offense_months = 12              # 7 CFR 273.16(e)(1)
second_offense_months = 24             # 7 CFR 273.16(e)(2)
third_offense_permanent = true         # 7 CFR 273.16(e)(3)
trafficking_permanent = true           # 7 CFR 273.16(e)

[appeals]
adh_notice_advance_days = 30           # 7 CFR 273.16(b)

Steps

Step 1: Add Parameters to jurisdiction.toml

Files: rulesets/georgia/jurisdiction.toml

Add the sections defined above under Design. Each key includes an inline comment citing the federal regulation it implements. Values match current hardcoded literals.

Step 2: Extract FPL Table from income_threshold.rs

Files: services/canopy-renewals/src/income_threshold.rs, rulesets/federal/fpl-2026.json

The existing rulesets/federal/fpl-2026.json already contains FPL data (verify format). Refactor income_threshold.rs to:

  • Accept an FplTable struct (or &serde_json::Value) loaded from JSON at startup

  • Replace fpl_for_household_size() match arms with a table lookup

  • Add the per-additional-person increment as a field (additional_person_increment)

  • The 130% gross income limit multiplier is already in jurisdiction.toml as bbce_gross_income_limit_pct_fpl; reuse it

Step 3: Parameterize Expedited Screening

Files: services/canopy-applications/src/expedited.rs

Create an ExpeditedParams struct:

pub struct ExpeditedParams {
    pub low_income_limit_cents: i64,
    pub liquid_assets_limit_cents: i64,
}

Change screen_expedited() signature to accept &ExpeditedParams. Replace literals 15000 and 10000 with params fields.

Step 4: Parameterize Certification Period Timing

Files: services/canopy-renewals/src/certification.rs

Create a CertificationParams struct:

pub struct CertificationParams {
    pub elderly_disabled_threshold_months: i32,
    pub interim_contact_months: u32,
    pub renewal_notice_advance_days: i64,
    pub second_renewal_notice_advance_days: i64,
}

Pass &CertificationParams into certification_type(), interim_contact_due_date(), renewal_notice_date(), and second_renewal_notice_date().

Step 5: Parameterize Scheduler Lookahead

Files: services/canopy-renewals/src/scheduler.rs

Change run_daily_check() to accept the renewal notice advance days from CertificationParams (same struct as Step 4). Replace the literal 75 with params.renewal_notice_advance_days.

Step 6: Parameterize IPV Penalty Schedule

Files: services/canopy-appeals/src/ipv/penalties.rs

Create an IpvPenaltyParams struct:

pub struct IpvPenaltyParams {
    pub first_offense_months: u32,
    pub second_offense_months: u32,
    pub third_offense_permanent: bool,
    pub trafficking_permanent: bool,
}

Pass into calculate_disqualification_period(). Replace literals 12, 24 with params fields. The permanent flag for 3rd+ offense and trafficking come from params.

Step 7: Parameterize ADH Notice and Issuance Deadlines

Files: services/canopy-appeals/src/ipv/workflow.rs, services/canopy-enrollment/src/issuance.rs

For workflow.rs: add adh_notice_advance_days: i64 parameter to validate_30_day_notice().

For issuance.rs: create an IssuanceParams struct:

pub struct IssuanceParams {
    pub expedited_days: i64,
    pub standard_days: i64,
    pub expungement_days: i64,
}

Pass into initial_issuance_due_date() and benefit_expiry_date().

Step 8: Parameterize Certification Periods in determine.rs

Files: services/canopy-snap/src/determine.rs

Lines 267-268 use hardcoded Months::new(6) and Months::new(5) for expiration_date and renewal_date. Add certification_months and renewal_offset_months to the existing SnapParameters struct (or create a separate DeterminationTimingParams). Load from jurisdiction.toml at startup.

Step 9: Update Tests

All affected unit tests must be updated to construct parameter structs explicitly. Test values should match the jurisdiction.toml defaults so test assertions remain unchanged. This verifies the parameterization is transparent.

Step 10: Full Test Battery

Run cargo xtask test to verify all unit and integration tests pass. Run cargo xtask dev reload --shared-db and re-run integration tests against devstack.

Files Touched

File Change

rulesets/georgia/jurisdiction.toml

Add [snap.expedited], [snap.certification], [snap.issuance], [snap.determination], [snap.ipv] sections; add adh_notice_advance_days to [appeals]

services/canopy-renewals/src/income_threshold.rs

Replace hardcoded FPL table with loaded FplTable struct

services/canopy-applications/src/expedited.rs

Accept ExpeditedParams, remove hardcoded $150/$100

services/canopy-renewals/src/certification.rs

Accept CertificationParams, remove hardcoded 22/6/75/30

services/canopy-renewals/src/scheduler.rs

Accept renewal lookahead from CertificationParams

services/canopy-appeals/src/ipv/penalties.rs

Accept IpvPenaltyParams, remove hardcoded 12/24/permanent

services/canopy-appeals/src/ipv/workflow.rs

Accept adh_notice_advance_days parameter

services/canopy-enrollment/src/issuance.rs

Accept IssuanceParams, remove hardcoded 7/30/365

services/canopy-snap/src/determine.rs

Accept cert/renewal months from params, remove hardcoded 6/5

Verification

  1. cargo nextest run --workspace --lib — unit tests pass

  2. cargo xtask dev reload --shared-db — devstack rebuilds

  3. cargo nextest run --workspace — integration tests pass

  4. cargo xtask e2e — E2E tests pass

  5. Grep for remaining hardcoded federal values: rg '(15000|10000|Months::new\(12\)|Months::new\(24\)|Duration::days\(30\)|Duration::days\(7\)|Duration::days\(365\)|Duration::days\(75\))' services/ should return zero matches outside of test code

Documentation Updates

  • .claude/docs/services.md — note parameter loading for affected services

  • CHANGELOG.adoc — entry under == Unreleased

  • .claude/CLAUDE.md — no changes needed (ADR-003 already documented)

Edit this page · default