ADR-006: Jurisdiction-Agnostic Ruleset Organization
On this page
Status
Accepted
rulesets/test-min/, rulesets/test-max/) that exercise it, and a federal option registry (compliance/federal-options/) that enumerates the state-option layer machine-checkably.
Context
ADR-003 established that all eligibility logic lives in versioned JDM ruleset files evaluated by canopy-rules.
The current ruleset path is rulesets/georgia/, which works for Georgia DHS but implies that other jurisdictions would need to fork the entire ruleset directory or modify Georgia’s rules.
Public benefit programs are administered under a combination of:
-
Federal floor rules — mandated by statute and regulation; no state option (e.g., gross income test at 130% FPL is the federal floor for SNAP; the net income test at 100% FPL is mandatory)
-
Federal parameters — set annually by federal agencies (e.g., FPL thresholds, maximum SNAP allotments, standard deductions); apply uniformly across all jurisdictions
-
State options — explicit state elections that can be exercised independently (e.g., BBCE, simplified reporting, interview waiver, vehicle exclusion methodology)
-
State-set values — thresholds and amounts that states determine entirely (e.g., TANF benefit amounts, TANF income limits, Medicaid income limits above federal floor)
-
State-specific provisions — unique to a jurisdiction (e.g., Georgia Pathways 1115 waiver work requirement)
A jurisdiction deploying Canopy needs to: * Use the correct federal parameters for the current fiscal year * Configure their elected state options * Set their own state-determined thresholds * Add any jurisdiction-specific provisions
The question is how to structure the ruleset repository to support this without requiring jurisdictions to copy-edit an entire ruleset library.
Decision
1. Ruleset directory structure
Each jurisdiction has its own directory under rulesets/:
rulesets/
├── federal/ # Federal parameters (FPL tables, max allotments)
│ ├── fpl-2026.json # FY2026 Federal Poverty Level table by household size
│ ├── snap-allotments-2026.json # FY2026 SNAP maximum monthly allotments
│ ├── snap-deductions-2026.json # FY2026 SNAP standard deductions, shelter caps
│ └── ...
├── georgia/ # Georgia DHS jurisdiction-specific rulesets
│ ├── snap-eligibility.json
│ ├── snap-benefit-calculation.json
│ ├── snap-categorical-eligibility.json
│ ├── snap-abawd.json
│ ├── tanf-eligibility.json
│ ├── tanf-benefit-calculation.json
│ ├── tanf-work-requirements.json
│ ├── medicaid-magi.json
│ ├── medicaid-non-magi.json
│ ├── medicaid-eligibility-hierarchy.json
│ ├── chip-eligibility.json
│ ├── caps-eligibility.json
│ └── wic-eligibility.json
└── {other-jurisdiction}/ # Future jurisdictions
└── ...
2. Federal parameters as shared input data
Federal parameters (FPL thresholds, SNAP maximum allotments, standard deductions) are not hardcoded in jurisdiction rulesets.
They are stored in rulesets/federal/ as versioned JSON data files.
canopy-rules loads federal parameter files at startup and makes them available as named inputs to any ruleset evaluation.
A jurisdiction ruleset references federal parameters by name:
{
"nodes": [
{
"type": "inputNode",
"id": "federal_params",
"name": "Federal Parameters",
"source": "federal/snap-deductions-2026"
},
...
]
}
Federal parameter files are updated annually when FNS publishes new figures (typically each October). A federal parameter update does not require changes to any jurisdiction ruleset — the rulesets reference the parameter file by name, and the parameter file is replaced.
canopy-rules supports a federal_year configuration parameter (default: current fiscal year) to select which federal parameter vintage to use.
This allows testing future-year parameters before they take effect.
3. Jurisdiction configuration
Each jurisdiction that deploys Canopy provides a jurisdiction.toml configuration file (loaded by canopy-rules at startup):
[jurisdiction]
name = "georgia"
display_name = "Georgia Department of Human Services"
admin_unit_label = "County" # or "Region", "District", "Chapter"
federal_year = 2026 # which vintage of federal parameters to use
[snap]
bbce_enabled = true
bbce_income_limit_pct_fpl = 130 # Georgia: 130% (same as gross income limit)
bbce_asset_test_eliminated = true
simplified_reporting = true
interview_waiver_enabled = true
vehicle_exclusion_method = "one_per_licensed_adult" # or "fmv_cap"
standard_certification_months = 12
elderly_disabled_certification_months = 24
medical_deduction_standard_enabled = false # Georgia uses actual expenses
[tanf]
income_limit_pct_fpl = 50
max_monthly_grant_family_3 = 280 # USD
time_limit_months = 60
[medicaid]
expansion_type = "pathways_1115_waiver" # "full_expansion", "no_expansion", "pathways_1115_waiver"
pathways_work_requirement_hours_per_month = 80
chip_upper_income_limit_pct_fpl = 247
[caps]
income_limit_pct_smi = 85 # Federal ceiling; Georgia may set lower
Jurisdiction configuration is read by canopy-rules and injected as named inputs to ruleset evaluations alongside federal parameters.
4. Ruleset versioning
Each ruleset file carries a version header:
{
"version": "2026.1.0",
"jurisdiction": "georgia",
"program": "snap",
"effective_date": "2026-10-01",
"description": "SNAP eligibility rules for Georgia, FY2026",
"nodes": [...]
}
canopy-rules stores the version string in the rule_evaluations audit table with every evaluation.
When a policy change triggers a ruleset update, the version is incremented.
Older ruleset versions remain importable for retrospective QC review and appeals.
Version format: {fiscal_year}.{major}.{minor}
* fiscal_year — changes when federal parameters change (annually)
* major — changes when eligibility logic changes (new categories, new deduction types)
* minor — changes when values change within existing logic (threshold adjustments, error corrections)
5. Adding a new jurisdiction
A new jurisdiction deploying Canopy:
-
Creates
rulesets/{jurisdiction}/directory -
Copies the
rulesets/georgia/ruleset templates as a starting point -
Creates
rulesets/{jurisdiction}/jurisdiction.tomlwith their configuration -
Updates threshold values and state option flags in
jurisdiction.toml -
Modifies or extends ruleset files for any jurisdiction-specific provisions (e.g., a unique categorical eligibility pathway)
-
Does not need to modify any Rust code
The only code change a new jurisdiction requires is adding their jurisdiction value to the CANOPY_JURISDICTION environment variable in their deployment configuration.
6. canopy-rules loading behavior
At startup, canopy-rules:
-
Reads
CANOPY_JURISDICTIONenvironment variable (default:georgia) -
Loads
jurisdiction.tomlfromrulesets/{jurisdiction}/jurisdiction.toml -
Loads all federal parameter files for the configured
federal_year -
Imports all
*.jsonruleset files fromrulesets/{jurisdiction}/ -
Makes federal parameters and jurisdiction config available as named inputs in all evaluations
Rulesets are hot-reloadable: PUT /v1/rulesets/{name} replaces a ruleset in memory and database without service restart (for policy updates).
Rationale
Why per-jurisdiction directories instead of parameterized templates?
An alternative considered was a single set of "template" rulesets with jurisdiction parameters injected at evaluation time. This was rejected because:
-
JDM rulesets are not designed as parameterized templates — they are decision tables
-
Jurisdiction-specific provisions (e.g., Georgia Pathways work requirement) are structural additions, not parameter substitutions
-
A jurisdiction with highly divergent rules (tribal TANF, for example) would need to override most of the template anyway
-
Per-jurisdiction directories make the scope of jurisdictional changes obvious from the file tree
Why jurisdiction.toml for configuration instead of more JDM files?
Threshold values and option flags are configuration, not decision logic.
Putting them in jurisdiction.toml (a structured config file) rather than JDM makes them:
-
Easier to read and audit (TOML is more readable than JDM JSON for named values)
-
Faster to update (changing a TOML value doesn’t require re-importing a JDM file)
-
Consistent across all rulesets in a jurisdiction (one file, not twelve)
-
Easier to diff between jurisdictions or between fiscal years
Why fiscal-year versioning for federal parameters?
SNAP allotments, FPL thresholds, and standard deductions are updated each October 1 (start of federal fiscal year). Versioning by fiscal year makes it unambiguous which parameter vintage applies to a given evaluation. This is critical for QC review: a case evaluated in FY2025 must be reviewable with FY2025 parameters, even after FY2026 parameters are loaded.
Consequences
Benefits
-
Zero code changes to deploy Canopy in a new jurisdiction — only ruleset files and
jurisdiction.toml -
Federal parameter updates require only replacing one JSON file per parameter type per year
-
Jurisdiction-specific provisions are isolated to one directory — no risk of one jurisdiction’s rules affecting another
-
Audit trail includes ruleset version — every determination is reproducible from historical ruleset versions
-
OpenStack analogy holds: Canopy is the platform;
rulesets/{jurisdiction}/is the configuration layer
Costs and risks
-
Ruleset library grows with each new jurisdiction — needs governance to prevent divergence in logic that should be shared
-
Testing must cover all jurisdiction profiles, not just Georgia — CI must parameterize integration tests by jurisdiction
-
Federal parameter files must be maintained annually — risk of forgetting to update them before October 1 cutover
Out of scope
-
Automated FPL table import from HHS API — future enhancement
-
Multi-jurisdiction single deployment (one Canopy instance serving multiple states) — not supported; requires separate deployments
-
Tribal TANF-specific adaptations — will be addressed when the first tribal TANF jurisdiction adopts Canopy