diff --git a/packages/populace-build/src/populace/build/gates.py b/packages/populace-build/src/populace/build/gates.py index cbd374bc..47038d5b 100644 --- a/packages/populace-build/src/populace/build/gates.py +++ b/packages/populace-build/src/populace/build/gates.py @@ -228,6 +228,7 @@ class TargetCoverageRequirement: accepted_families: tuple[str, ...] = () required_measures: tuple[str, ...] = () required_metadata: tuple[tuple[str, str], ...] = () + required_metadata_keys: tuple[str, ...] = () min_matches: int = 1 notes: str = "" @@ -264,6 +265,12 @@ def __post_init__(self) -> None: f"TargetCoverageRequirement {self.requirement_id!r}: " f"metadata requirements need non-empty keys and values: {bad_metadata}." ) + bad_metadata_keys = [key for key in self.required_metadata_keys if not key] + if bad_metadata_keys: + raise ValueError( + f"TargetCoverageRequirement {self.requirement_id!r}: " + "metadata key requirements need non-empty keys." + ) @dataclass(frozen=True) @@ -354,9 +361,11 @@ def _matches_target_requirement( and entry.measure not in requirement.required_measures ): return False - return all( + if not all( entry.metadata.get(key) == value for key, value in requirement.required_metadata - ) + ): + return False + return all(key in entry.metadata for key in requirement.required_metadata_keys) def target_profile_coverage_gate( diff --git a/packages/populace-build/src/populace/build/us_runtime/fiscal_targets.py b/packages/populace-build/src/populace/build/us_runtime/fiscal_targets.py index 253e5e14..7bffed8a 100644 --- a/packages/populace-build/src/populace/build/us_runtime/fiscal_targets.py +++ b/packages/populace-build/src/populace/build/us_runtime/fiscal_targets.py @@ -2785,6 +2785,19 @@ def _str_at(obj: object, *path: str) -> str: accepted_families=("usda_snap",), required_metadata=(("target_role", "snap_total"),), ), + TargetCoverageRequirement( + requirement_id="snap_state_benefits", + label="USDA SNAP state benefit totals", + accepted_families=("usda_snap",), + required_metadata=(("target_role", "snap_total"),), + required_metadata_keys=("state_fips",), + min_matches=51, + notes=( + "FNS FY69-to-current state activity rows: 50 states plus DC. " + "Territory rows (Guam, USVI) resolve no PolicyEngine state FIPS " + "and are skipped at reference compilation, so they do not count." + ), + ), TargetCoverageRequirement( requirement_id="unemployment_compensation_total", label="SOI unemployment compensation total", diff --git a/packages/populace-build/tests/test_gates.py b/packages/populace-build/tests/test_gates.py index f8f136f3..7811d23c 100644 --- a/packages/populace-build/tests/test_gates.py +++ b/packages/populace-build/tests/test_gates.py @@ -874,6 +874,46 @@ def test_requirement_metadata_must_match_when_declared(self) -> None: ) assert with_metadata.passed + def test_required_metadata_keys_must_be_present(self) -> None: + requirement = TargetCoverageRequirement( + requirement_id="snap_state_benefits", + label="SNAP state benefit totals", + accepted_families=("usda_snap",), + required_metadata_keys=("state_fips",), + ) + missing_key = target_profile_coverage_gate( + [ + { + "name": "usda_snap.fy2024.national_benefits.total_benefits", + "family": "usda_snap", + "metadata": {"target_role": "snap_total"}, + } + ], + [requirement], + ) + assert not missing_key.passed + + with_key = target_profile_coverage_gate( + [ + { + "name": "usda_snap.fy2024.state_benefits.wro.ca.total_benefits", + "family": "usda_snap", + "metadata": {"target_role": "snap_total", "state_fips": "06"}, + } + ], + [requirement], + ) + assert with_key.passed + + def test_required_metadata_keys_reject_empty_key(self) -> None: + with pytest.raises(ValueError, match="non-empty keys"): + TargetCoverageRequirement( + requirement_id="bad", + label="Bad requirement", + accepted_families=("usda_snap",), + required_metadata_keys=("",), + ) + def test_minimum_match_count_is_enforced(self) -> None: requirement = TargetCoverageRequirement( requirement_id="state_income_tax", diff --git a/packages/populace-build/tests/test_us_fiscal_targets.py b/packages/populace-build/tests/test_us_fiscal_targets.py index 603794ab..85b3ba27 100644 --- a/packages/populace-build/tests/test_us_fiscal_targets.py +++ b/packages/populace-build/tests/test_us_fiscal_targets.py @@ -639,9 +639,7 @@ def test_extra_support_exclusions_drop_per_run_without_touching_registry() -> No baseline = compile_us_fiscal_target_registry( facts, allow_unaged_dollar_targets=True ) - baseline_ids = { - spec.metadata["ledger_source_record_id"] for spec in baseline.specs - } + baseline_ids = {spec.metadata["ledger_source_record_id"] for spec in baseline.specs} # Without the per-run exclusion, California IS an active target. assert excluded_source_record_id in baseline_ids @@ -699,6 +697,49 @@ def test_extra_support_exclusions_reject_empty_reason() -> None: pass +def test_state_level_snap_benefits_fact_compiles_to_state_hard_target() -> None: + california_source_record_id = ( + "usda_snap.fy2024.state_benefits.wro.ca.total_benefits" + ) + guam_source_record_id = "usda_snap.fy2024.state_benefits.wro.gu.total_benefits" + facts = [ + *packaged_reference_facts(), + _dynamic_ledger_fact( + source_record_id=california_source_record_id, + source_name="usda_snap", + measure_id="total_benefits", + value=12_000_000_000, + geography_level="state", + geography_id="0400000US06", + groupby_value_id="ca", + ), + _dynamic_ledger_fact( + source_record_id=guam_source_record_id, + source_name="usda_snap", + measure_id="total_benefits", + value=250_000_000, + geography_level="state", + geography_id="0400000US66", + groupby_value_id="gu", + ), + ] + + registry = compile_us_fiscal_target_registry(facts) + + by_source_record_id = { + spec.metadata["ledger_source_record_id"]: spec for spec in registry.specs + } + assert california_source_record_id in by_source_record_id + california = by_source_record_id[california_source_record_id] + assert california.family == "usda_snap" + assert california.metadata["target_role"] == "snap_total" + assert california.metadata["base_variable"] == "snap" + assert california.metadata["state_fips"] == "06" + assert california.value == 12_000_000_000 + # Guam has no PolicyEngine state FIPS, so the row must not become a target. + assert guam_source_record_id not in by_source_record_id + + def test_weight_dependent_medicaid_spending_is_validation_only() -> None: source_record_id = ( "cms_nhe.cy2024.medicaid_title_xix_expenditures." @@ -3121,6 +3162,7 @@ def test_structured_income_tax_positive_does_not_satisfy_total_tax() -> None: *complete_deduction_amount_rows(), *complete_program_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3145,6 +3187,7 @@ def test_soi_income_tax_liability_satisfies_total_tax() -> None: *complete_deduction_amount_rows(), *complete_program_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3192,6 +3235,7 @@ def test_jct_target_name_without_simple_reform_metadata_fails() -> None: *complete_deduction_amount_rows(), *complete_program_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *(spec.target_name for spec in US_JCT_TAX_EXPENDITURE_REFORMS), ] @@ -3214,6 +3258,7 @@ def test_jct_revenue_loss_targets_do_not_satisfy_deduction_amount_controls() -> *complete_income_source_rows(), *complete_program_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3259,6 +3304,7 @@ def test_medicaid_chip_requirement_needs_combined_enrollment_role() -> None: if row["metadata"]["target_role"] != "medicaid_chip_enrollment" ], *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3279,6 +3325,7 @@ def test_medicaid_requirement_needs_enrollment_role() -> None: *complete_income_source_rows(), *complete_deduction_amount_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3314,6 +3361,7 @@ def test_chip_requirement_needs_direct_chip_role() -> None: if row["metadata"]["target_role"] != "chip_enrollment" ], *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -3990,6 +4038,7 @@ def complete_coverage_targets() -> list[dict[str, object]]: *complete_deduction_amount_rows(), *complete_program_rows(), *complete_state_income_tax_rows(45), + *complete_snap_state_rows(), *complete_population_age_rows(), *complete_jct_rows(), ] @@ -4100,6 +4149,22 @@ def complete_state_income_tax_rows(count: int) -> list[dict[str, object]]: ] +def complete_snap_state_rows(count: int = 51) -> list[dict[str, object]]: + # 50 states + DC; state_fips presence is what the snap_state_benefits + # requirement demands (populace #255/#256). + return [ + { + "name": f"usda_snap.fy2024.state_benefits.state_{i:02d}.total_benefits", + "measure": ( + f"usda_snap.fy2024.state_benefits.state_{i:02d}.total_benefits" + ), + "family": "usda_snap", + "metadata": {"target_role": "snap_total", "state_fips": f"{i + 1:02d}"}, + } + for i in range(count) + ] + + def complete_population_age_rows() -> list[dict[str, object]]: national = [ {