Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ changelog.d/medicaid-ce-exclusions.md
- **When adding a new program**: add an entry with `id`, `name`, `full_name`, `category`, `agency`, `status`, `coverage`, `variable`, `parameter_prefix`
- **When extending year coverage**: update the entry's year field — most entries use `verified_start_year`, a few use a `verified_years` range (e.g., `"2022-2026"`) — after verifying parameters and tests cover the new year
- **When adding state implementations**: add to `state_implementations` list under the parent federal program
- **Status values**: `complete`, `partial`, `in_progress`
- **Status values**: `complete`, `partial`, `in_progress`. There is no not-started value: when an `in_progress` entry's PR closes unmerged and no code is on main, remove the entry (and its state from `coverage`)
- `policyengine_us/tests/test_programs_registry.py` checks statuses, state codes, and that every `variable` exists and every `parameter_prefix` resolves in the parameter tree. Never add keys to its `KNOWN_UNRESOLVED` list; delete them as entries are fixed
- Keep entries sorted by: Taxes, then Benefits by agency (USDA, HHS, SSA, HUD, FCC, ED, DOE), then State, then Local

## State Program Patterns
Expand Down
1 change: 1 addition & 0 deletions changelog.d/fix-liheap-registry.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Program registry LIHEAP fixes: DC LIHEAP now points at `dc_liheap_payment` (`dc_liheap` does not exist); Riverside County LIHEAP's parameter prefix is `gov.local.ca.riv.cap.liheap`; the Oregon LIHEAP entry, which had no code on main or open PR, is removed; DC, Massachusetts and Illinois gain parameter prefixes; and Texas CEAP's note gives the SMI limit's actual years. A new registry test checks that every listed variable and parameter prefix exists.
15 changes: 7 additions & 8 deletions policyengine_us/programs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -802,27 +802,26 @@ programs:
category: Benefits
agency: HHS
status: partial
coverage: OR, DC, Riverside County, MA, IL, IN, KS, KY, MS, NC, NE, TX
coverage: DC, Riverside County, MA, IL, IN, KS, KY, MS, NC, NE, TX
state_implementations:
- state: OR
status: in_progress
name: LIHEAP
full_name: Low Income Home Energy Assistance Program (Oregon)
- state: DC
status: complete
name: DC LIHEAP
full_name: DC Low Income Home Energy Assistance Program
variable: dc_liheap
variable: dc_liheap_payment
parameter_prefix: gov.states.dc.doee.liheap
- state: MA
status: complete
name: Massachusetts LIHEAP
full_name: Massachusetts Low Income Home Energy Assistance Program
variable: ma_liheap
parameter_prefix: gov.states.ma.doer.liheap
- state: IL
status: complete
name: Illinois LIHEAP
full_name: Illinois Low Income Home Energy Assistance Program
variable: il_liheap
parameter_prefix: gov.states.il.dceo.liheap
- state: IN
status: partial
name: Indiana EAP
Expand Down Expand Up @@ -877,7 +876,7 @@ programs:
full_name: Texas Comprehensive Energy Assistance Program
variable: tx_ceap
parameter_prefix: gov.states.tx.tdhca.ceap
notes: Models the FPG income limit, the FY2024-only SMI limit, benefit amounts, and categorical eligibility through TANF enrollment, SNAP eligibility, and SSI receipt. Not modeled - net rather than gross gambling winnings, the Medicare premium deduction from counted Social Security, and the exclusion of means-tested veterans' pensions from counted income (10 TAC 6.4(c))
notes: Models the 150% FPG income limit, the alternative 60% SMI limit from the FY2024 state plan (applied through 2024), benefit amounts, and categorical eligibility through TANF enrollment, SNAP eligibility, and SSI receipt. Not modeled - net rather than gross gambling winnings, the Medicare premium deduction from counted Social Security, and the exclusion of means-tested veterans' pensions from counted income (10 TAC 6.4(c))

# --- SSA programs ---
- id: ssi
Expand Down Expand Up @@ -1718,7 +1717,7 @@ programs:
status: complete
coverage: Riverside County
variable: ca_riv_liheap_eligible
parameter_prefix: gov.local.ca.riv.liheap
parameter_prefix: gov.local.ca.riv.cap.liheap
notes: Only includes eligibility rules

- id: share
Expand Down
148 changes: 148 additions & 0 deletions policyengine_us/tests/test_programs_registry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"""Structural guards for the program registry (programs.yaml).

The registry is served through the /us/metadata API as ``modelled_policies``
and renders the model coverage page, so every model reference in it has to
point at something the model defines. Two failure modes have shipped before:
1. A ``variable`` that the model does not define (DC LIHEAP listed
``dc_liheap``, but the benefit variable is ``dc_liheap_payment``).
2. A ``parameter_prefix`` that resolves to no parameter node (Riverside County
LIHEAP listed ``gov.local.ca.riv.liheap``; its parameters live under
``gov.local.ca.riv.cap.liheap``).
"""

from collections import Counter
from pathlib import Path

import pytest
import yaml

REGISTRY = Path(__file__).parent.parent / "programs.yaml"

STATUSES = {"complete", "partial", "in_progress"}

# References that did not resolve when this guard was added, keyed by
# (entry label, field). Fix the registry entry and delete its key here; do
# not add new keys.
KNOWN_UNRESOLVED = {
("csfp", "variable"),
("fdpir", "parameter_prefix"),
("social_security", "parameter_prefix"),
("clean_vehicle_credits", "variable"),
("dc_power", "parameter_prefix"),
("dc_gac", "parameter_prefix"),
("la_expectant_parent", "parameter_prefix"),
("sf_wftc", "variable"),
("montgomery_county_eitc", "variable"),
("montgomery_county_eitc", "parameter_prefix"),
}


def registry_entries():
"""(label, state or None, entry) for every program and state implementation.

A state implementation's label includes its name, since a program can list
more than one implementation for a state (Washington's two ECEAPs).
"""
programs = yaml.safe_load(REGISTRY.read_text())["programs"]
entries = []
for program in programs:
entries.append((program["id"], None, program))
for implementation in program.get("state_implementations") or []:
state = implementation["state"]
label = f"{program['id']}/{state}/{implementation['name']}"
entries.append((label, state, implementation))
return entries


ENTRIES = registry_entries()
ENTRIES_BY_LABEL = {label: entry for label, _, entry in ENTRIES}


def references(field):
return [
pytest.param(label, entry[field], id=label)
for label, _, entry in ENTRIES
if field in entry
]


@pytest.fixture(scope="module")
def system():
from policyengine_us.system import system

return system


def parameter_prefix_resolves(system, prefix):
try:
system.parameters.get_child(prefix)
except ValueError:
return False
return True


def test_entry_labels_are_unique():
counts = Counter(label for label, _, _ in ENTRIES)
duplicates = sorted(label for label, count in counts.items() if count > 1)
assert not duplicates, f"Duplicate program ids or implementations: {duplicates}"


@pytest.mark.parametrize(
"label, entry",
[pytest.param(label, entry, id=label) for label, _, entry in ENTRIES],
)
def test_status_is_known(label, entry):
assert entry["status"] in STATUSES, (
f"{label} has status {entry['status']!r}; the registry uses "
f"{sorted(STATUSES)}. List a program only once work on it has started."
)


def test_state_implementations_name_valid_states():
from policyengine_us.variables.household.demographic.geographic.state_code import (
StateCode,
)

valid = {state.value for state in StateCode}
invalid = sorted(
label for label, state, _ in ENTRIES if state is not None and state not in valid
)
assert not invalid, f"Unknown state codes: {invalid}"


@pytest.mark.parametrize("label, variable", references("variable"))
def test_variable_is_defined(system, label, variable):
if (label, "variable") in KNOWN_UNRESOLVED:
pytest.skip("listed in KNOWN_UNRESOLVED")
assert variable in system.variables, (
f"{label} names variable {variable!r}, which the model does not define."
)


@pytest.mark.parametrize("label, prefix", references("parameter_prefix"))
def test_parameter_prefix_resolves(system, label, prefix):
if (label, "parameter_prefix") in KNOWN_UNRESOLVED:
pytest.skip("listed in KNOWN_UNRESOLVED")
assert parameter_prefix_resolves(system, prefix), (
f"{label} names parameter_prefix {prefix!r}, which resolves to no "
"parameter node."
)


def test_known_unresolved_references_are_still_unresolved(system):
"""Keep the exemption list from outliving the bugs it records."""
stale = []
for label, field in sorted(KNOWN_UNRESOLVED):
entry = ENTRIES_BY_LABEL.get(label)
if entry is None or field not in entry:
stale.append((label, field))
elif field == "variable" and entry[field] in system.variables:
stale.append((label, field))
elif field == "parameter_prefix" and parameter_prefix_resolves(
system, entry[field]
):
stale.append((label, field))
assert not stale, (
"These KNOWN_UNRESOLVED keys now resolve or no longer exist; "
f"delete them: {stale}"
)
Loading