Plan: ADR-003 Compliance Remediation
On this page
- Status
- Context
- Scope
- Design
- Steps
- Step 1: Add Parameters to jurisdiction.toml
- Step 2: Extract FPL Table from income_threshold.rs
- Step 3: Parameterize Expedited Screening
- Step 4: Parameterize Certification Period Timing
- Step 5: Parameterize Scheduler Lookahead
- Step 6: Parameterize IPV Penalty Schedule
- Step 7: Parameterize ADH Notice and Issuance Deadlines
- Step 8: Parameterize Certification Periods in determine.rs
- Step 9: Update Tests
- Step 10: Full Test Battery
- Files Touched
- Verification
- Documentation Updates
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 fromDeductionParams(jurisdiction.toml) -
canopy-snap/src/abawd.rs— loads ABAWD params from jurisdiction.toml -
canopy-snap/src/categorical.rs— loadsBbceConfigfrom jurisdiction.toml -
canopy-snap/src/determine.rsincome 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:
-
Add TOML keys to
rulesets/georgia/jurisdiction.toml -
Add corresponding fields to the service’s parameter struct
-
Pass params into functions that currently use literals
-
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
FplTablestruct (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.
Files Touched
| File | Change |
|---|---|
|
Add |
|
Replace hardcoded FPL table with loaded |
|
Accept |
|
Accept |
|
Accept renewal lookahead from |
|
Accept |
|
Accept |
|
Accept |
|
Accept cert/renewal months from params, remove hardcoded 6/5 |
Verification
-
cargo nextest run --workspace --lib— unit tests pass -
cargo xtask dev reload --shared-db— devstack rebuilds -
cargo nextest run --workspace— integration tests pass -
cargo xtask e2e— E2E tests pass -
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)