ADR-006: Jurisdiction-Agnostic Ruleset Organization

On this page

Status

Accepted

NOTE
Extended by ADR-032 (2026-06-10): the option-space taxonomy below (federal floor / federal parameters / state options / state values / state provisions) gains synthetic test jurisdictions (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:

  1. 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)

  2. Federal parameters — set annually by federal agencies (e.g., FPL thresholds, maximum SNAP allotments, standard deductions); apply uniformly across all jurisdictions

  3. State options — explicit state elections that can be exercised independently (e.g., BBCE, simplified reporting, interview waiver, vehicle exclusion methodology)

  4. State-set values — thresholds and amounts that states determine entirely (e.g., TANF benefit amounts, TANF income limits, Medicaid income limits above federal floor)

  5. 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:

Example: snap-eligibility.json referencing federal parameters
{
  "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:

  1. Creates rulesets/{jurisdiction}/ directory

  2. Copies the rulesets/georgia/ ruleset templates as a starting point

  3. Creates rulesets/{jurisdiction}/jurisdiction.toml with their configuration

  4. Updates threshold values and state option flags in jurisdiction.toml

  5. Modifies or extends ruleset files for any jurisdiction-specific provisions (e.g., a unique categorical eligibility pathway)

  6. 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:

  1. Reads CANOPY_JURISDICTION environment variable (default: georgia)

  2. Loads jurisdiction.toml from rulesets/{jurisdiction}/jurisdiction.toml

  3. Loads all federal parameter files for the configured federal_year

  4. Imports all *.json ruleset files from rulesets/{jurisdiction}/

  5. 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

Edit this page · default