From e705e436b840e07e5b7288da1688e0da67999942 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 09:11:11 -0400 Subject: [PATCH 1/6] Add input-column coverage gate primitive + derived release coverage manifest (#368) Generalizes the #350 required-source-columns mechanism to the full reference eCPS input surface. input_column_coverage_gate() fails a required column that is absent OR present-but-degenerate, with #286 cannot-rot reviewed exclusions. Manifest derived from the pinned eCPS parity reference + known-gaps register: 58 required (incl. the 3 SSI countable-resource assets with NO exclusion per #368) + 100 reviewed exclusions = the 158 populated eCPS input layers. Co-Authored-By: Claude Fable 5 --- .../src/populace/build/gates.py | 128 +++ .../us/release_input_coverage_manifest.json | 728 ++++++++++++++++++ ...uild_us_release_input_coverage_manifest.py | 196 +++++ 3 files changed, 1052 insertions(+) create mode 100644 packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json create mode 100644 tools/build_us_release_input_coverage_manifest.py diff --git a/packages/populace-build/src/populace/build/gates.py b/packages/populace-build/src/populace/build/gates.py index 46ffa855..6a53ebde 100644 --- a/packages/populace-build/src/populace/build/gates.py +++ b/packages/populace-build/src/populace/build/gates.py @@ -56,6 +56,7 @@ "export_surface_gate", "formula_owned_export_gate", "exported_nonzero_gate", + "input_column_coverage_gate", "input_mass_parity_gate", "macro_realism_gate", "nonconstant_columns_gate", @@ -1520,6 +1521,133 @@ def source_stage_input_coverage_gate( ) +def input_column_coverage_gate( + present_columns: Iterable[str], + *, + required_columns: Iterable[str], + degenerate_columns: Iterable[str] = (), + reviewed_exclusions: Mapping[str, str] | None = None, + name: str = "input_column_coverage", +) -> GateResult: + """Every declared-required input column is present AND carries signal. + + This is the release-blocking column-coverage contract of populace #368: a + release must persist every input column the reference eCPS exports, as a + real dataset key whose values are not all the engine default. It is the + generalization of ``assert_required_us_release_source_columns`` — which + guarded five SNAP/ACA/immigration columns — to the full eCPS input surface, + and it closes the gap the export-mass parity gate leaves open: parity only + covers the ~35 columns with a weighted-mass anchor, so an input the base + never imputed (SSI countable-resource assets, tips, overtime, education + credits, IRA/HSA — populace #340/#356/#361/#278) is *absent from the export + entirely* and every reform binding through it silently scores ~$0 while + every mass and parity gate still passes. + + A required column fails when it is absent from ``present_columns`` (the + engine defaults it at simulation time) or listed in ``degenerate_columns`` + (present as a key but every observed value equals the engine default, so + the export writer's default-broadcast makes it indistinguishable from + absence). Both are the same silent-zero failure and both are refused unless + the column carries a reviewed exclusion. + + Reviewed exclusions accept a known, tracked gap by name with a reason (and, + by convention, the tracking issue in the reason). They carry the #286 + "cannot rot" semantics: a reviewed exclusion whose column is now present + *and* non-degenerate is stale — the data caught up, so the column must be + promoted to a hard requirement — and fails the gate. An exclusion for a + column absent from this surface is dormant and only reported (release lines + persist different subsets). ``required_columns`` and + ``reviewed_exclusions`` must be disjoint: a column is either a hard + requirement or a tracked gap, never both. + + Args: + present_columns: Every input column the export persists as a key. + required_columns: Columns that must be present and non-degenerate. + degenerate_columns: The subset of present columns whose every observed + value equals the engine default (computed by the caller against the + engine's declared defaults). Columns not present are never here. + reviewed_exclusions: Column -> reason for tracked gaps allowed to be + absent or degenerate. Each needs a non-empty reason; a stale entry + fails the gate, a dormant entry is only reported. + name: Gate name for the manifest (defaults to + ``"input_column_coverage"``). + + Returns: + Pass iff every required column is present and non-degenerate, no + required column overlaps the exclusion register, and no exclusion is + stale. + + Raises: + ValueError: If a reviewed exclusion has an empty reason, or a column is + both required and a reviewed exclusion. + TypeError: If ``reviewed_exclusions`` is not a mapping. + """ + if not name: + raise ValueError("input column coverage gate name must be non-empty.") + exclusions = _reviewed_exclusion_reasons(reviewed_exclusions) + present = {str(column) for column in present_columns} + degenerate = {str(column) for column in degenerate_columns} & present + required = {str(column) for column in required_columns} + + both = sorted(required & set(exclusions)) + if both: + raise ValueError( + "A column cannot be both a hard requirement and a reviewed " + f"exclusion: {both}." + ) + + failures: list[str] = [] + missing: list[str] = [] + degenerate_required: list[str] = [] + for column in sorted(required): + if column not in present: + missing.append(column) + failures.append( + f"{column}: required eCPS input column is absent from the " + "export; the engine defaults it and every reform binding " + "through it scores ~$0. Impute it (carry it through the base " + "and selection) or record a reviewed exclusion with the " + "tracking issue." + ) + elif column in degenerate: + degenerate_required.append(column) + failures.append( + f"{column}: required eCPS input column is present but every " + "value equals the engine default; the export writer's " + "default-broadcast makes it indistinguishable from absence. " + "Impute it or record a reviewed exclusion with the tracking " + "issue." + ) + + signal_present = present - degenerate + stale = sorted(column for column in exclusions if column in signal_present) + if stale: + failures.append( + "Stale reviewed exclusions — the column carries signal now, " + f"promote it to a hard requirement: {stale}." + ) + dormant = sorted(column for column in exclusions if column not in present) + + return GateResult( + name=name, + passed=not failures, + failures=tuple(failures), + details={ + "required_columns": len(required), + "present_columns": len(present), + "missing": missing, + "degenerate_required": degenerate_required, + "reviewed_exclusions": { + column: reason + for column, reason in sorted(exclusions.items()) + if column not in dormant + }, + "stale_exclusions": stale, + "dormant_exclusions": dormant, + }, + ) + + def parity_gate( candidate_nonzero: Mapping[str, float], reference_nonzero: Mapping[str, float], diff --git a/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json new file mode 100644 index 00000000..7d329cdd --- /dev/null +++ b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json @@ -0,0 +1,728 @@ +{ + "columns": { + "age": { + "status": "required" + }, + "alimony_expense": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "alimony_income": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "attends_eligible_educational_institution_for_american_opportunity_credit": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (education credits understated).", + "status": "reviewed_exclusion" + }, + "auto_loan_balance": { + "issue": "PolicyEngine/populace#49", + "reason": "Vehicle-ownership / auto-loan input (populace#49 cites us-data #267/#281; see also #252 for the OBBBA auto-loan deduction); not yet on the frame.", + "status": "reviewed_exclusion" + }, + "auto_loan_interest": { + "issue": "PolicyEngine/populace#49", + "reason": "Vehicle-ownership / auto-loan input (populace#49 cites us-data #267/#281; see also #252 for the OBBBA auto-loan deduction); not yet on the frame.", + "status": "reviewed_exclusion" + }, + "bank_account_assets": { + "note": "SSI countable-resource asset input; required with NO reviewed exclusion per PolicyEngine/populace#368 so the gate fails until the asset stage is restored (Deliverable 2). Currently absent — this is the intended red gate.", + "status": "required" + }, + "block_geoid": { + "issue": "PolicyEngine/populace#38", + "reason": "Fine-grain geography code carried by the incumbent but not on the small national candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "bond_assets": { + "note": "SSI countable-resource asset input; required with NO reviewed exclusion per PolicyEngine/populace#368 so the gate fails until the asset stage is restored (Deliverable 2). Currently absent — this is the intended red gate.", + "status": "required" + }, + "business_is_sstb": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "casualty_loss": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "charitable_cash_donations": { + "status": "required" + }, + "charitable_non_cash_donations": { + "status": "required" + }, + "child_support_expense": { + "issue": "PolicyEngine/populace#32", + "reason": "HHS OCSE child-support received/paid SPM input; not yet on the frame.", + "status": "reviewed_exclusion" + }, + "child_support_received": { + "issue": "PolicyEngine/populace#32", + "reason": "HHS OCSE child-support received/paid SPM input; not yet on the frame.", + "status": "reviewed_exclusion" + }, + "congressional_district_geoid": { + "status": "required" + }, + "county_fips": { + "issue": "PolicyEngine/populace#38", + "reason": "Fine-grain geography code carried by the incumbent but not on the small national candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "cps_race": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "detailed_occupation_recode": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "disability_benefits": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "domestic_production_ald": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "educational_assistance": { + "issue": "PolicyEngine/populace#253", + "reason": "Educational-assistance income input tied to the education-credit surface; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "educator_expense": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "employer_sponsored_insurance_premiums": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "employment_income_before_lsr": { + "status": "required" + }, + "estate_income": { + "status": "required" + }, + "estate_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "farm_income": { + "status": "required" + }, + "farm_operations_income": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "farm_operations_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "farm_rent_income": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "farm_rent_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "financial_assistance": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "first_home_mortgage_balance": { + "status": "required" + }, + "first_home_mortgage_interest": { + "status": "required" + }, + "first_home_mortgage_origination_year": { + "status": "required" + }, + "has_american_opportunity_credit_1098_t_or_exception": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (education credits understated).", + "status": "reviewed_exclusion" + }, + "has_american_opportunity_credit_institution_ein": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (education credits understated).", + "status": "reviewed_exclusion" + }, + "has_champva_health_coverage_at_interview": { + "status": "required" + }, + "has_esi": { + "status": "required" + }, + "has_indian_health_service_coverage_at_interview": { + "status": "required" + }, + "has_marketplace_health_coverage": { + "status": "required" + }, + "has_marketplace_health_coverage_at_interview": { + "status": "required" + }, + "has_medicaid_health_coverage_at_interview": { + "status": "required" + }, + "has_never_worked": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "has_non_marketplace_direct_purchase_health_coverage_at_interview": { + "status": "required" + }, + "has_other_means_tested_health_coverage_at_interview": { + "status": "required" + }, + "has_tricare_health_coverage_at_interview": { + "status": "required" + }, + "has_va_health_coverage_at_interview": { + "status": "required" + }, + "health_insurance_premiums_without_medicare_part_b": { + "status": "required" + }, + "health_savings_account_ald": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "home_mortgage_interest": { + "status": "required" + }, + "hourly_wage": { + "issue": "PolicyEngine/populace#242", + "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", + "status": "reviewed_exclusion" + }, + "hours_worked_last_week": { + "issue": "PolicyEngine/populace#242", + "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", + "status": "reviewed_exclusion" + }, + "household_vehicles_owned": { + "issue": "PolicyEngine/populace#49", + "reason": "Vehicle-ownership / auto-loan input (populace#49 cites us-data #267/#281; see also #252 for the OBBBA auto-loan deduction); not yet on the frame.", + "status": "reviewed_exclusion" + }, + "household_vehicles_value": { + "issue": "PolicyEngine/populace#49", + "reason": "Vehicle-ownership / auto-loan input (populace#49 cites us-data #267/#281; see also #252 for the OBBBA auto-loan deduction); not yet on the frame.", + "status": "reviewed_exclusion" + }, + "household_weight": { + "issue": "PolicyEngine/populace#38", + "reason": "Incumbent stored a household_weight input column; Populace carries weights as a typed Frame weight vector, not a stored input variable. Classify as reported-observation vs reviewed-exclusion under #38.", + "status": "reviewed_exclusion" + }, + "immigration_status_str": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "investment_income_elected_form_4952": { + "issue": "PolicyEngine/populace#274", + "reason": "Capital-gains component detail (collectibles / unrecaptured 1250 / 4952 election / investment-interest) not yet split onto the candidate frame.", + "status": "reviewed_exclusion" + }, + "investment_interest_expense": { + "issue": "PolicyEngine/populace#274", + "reason": "Capital-gains component detail (collectibles / unrecaptured 1250 / 4952 election / investment-interest) not yet split onto the candidate frame.", + "status": "reviewed_exclusion" + }, + "is_blind": { + "status": "required" + }, + "is_computer_scientist": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_disabled": { + "status": "required" + }, + "is_enrolled_at_least_half_time_for_american_opportunity_credit": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (education credits understated).", + "status": "reviewed_exclusion" + }, + "is_executive_administrative_professional": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_farmer_fisher": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_female": { + "status": "required" + }, + "is_full_time_college_student": { + "status": "required" + }, + "is_hispanic": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_household_head": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_military": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_paid_hourly": { + "issue": "PolicyEngine/populace#242", + "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", + "status": "reviewed_exclusion" + }, + "is_pregnant": { + "status": "required" + }, + "is_pursuing_credential_for_american_opportunity_credit": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (education credits understated).", + "status": "reviewed_exclusion" + }, + "is_separated": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_surviving_spouse": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_union_member_or_covered": { + "issue": "PolicyEngine/populace#242", + "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", + "status": "reviewed_exclusion" + }, + "is_unmarried_partner_of_household_head": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "is_wic_at_nutritional_risk": { + "issue": "PolicyEngine/populace#312", + "reason": "WIC take-up / eligibility-screen input the incumbent seeds; not yet produced by Populace (take-up family).", + "status": "reviewed_exclusion" + }, + "keogh_distributions": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "long_term_capital_gains_before_response": { + "status": "required" + }, + "long_term_capital_gains_on_collectibles": { + "issue": "PolicyEngine/populace#274", + "reason": "Capital-gains component detail (collectibles / unrecaptured 1250 / 4952 election / investment-interest) not yet split onto the candidate frame.", + "status": "reviewed_exclusion" + }, + "miscellaneous_income": { + "status": "required" + }, + "net_worth": { + "issue": "PolicyEngine/populace#49", + "reason": "SCF-derived household asset/net-worth stock; not yet imputed onto the candidate frame (wealth imputation backlog).", + "status": "reviewed_exclusion" + }, + "non_qualified_dividend_income": { + "status": "required" + }, + "non_sch_d_capital_gains": { + "status": "required" + }, + "other_health_insurance_premiums": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "other_medical_expenses": { + "status": "required" + }, + "over_the_counter_health_expenses": { + "status": "required" + }, + "own_children_in_household": { + "status": "required" + }, + "partnership_s_corp_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "pre_subsidy_rent": { + "issue": "PolicyEngine/populace#32", + "reason": "HUD housing-assistance / tenure SPM input feeding capped housing subsidy; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "previous_year_income_available": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "qualified_bdc_income": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "qualified_dividend_income": { + "status": "required" + }, + "qualified_reit_and_ptp_income": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "qualified_tuition_expenses": { + "issue": "PolicyEngine/populace#253", + "reason": "American Opportunity / tuition education-credit input essentially unimputed on the candidate frame (#253).", + "status": "reviewed_exclusion" + }, + "real_estate_taxes": { + "status": "required" + }, + "receives_housing_assistance": { + "issue": "PolicyEngine/populace#32", + "reason": "HUD housing-assistance / tenure SPM input feeding capped housing subsidy; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "rental_income": { + "status": "required" + }, + "rental_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "salt_refund_income": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "selected_marketplace_plan_benchmark_ratio": { + "status": "required" + }, + "self_employment_income_before_lsr": { + "status": "required" + }, + "self_employment_income_last_year": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "self_employment_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "short_term_capital_gains": { + "status": "required" + }, + "social_security_dependents": { + "status": "required" + }, + "social_security_disability": { + "status": "required" + }, + "social_security_retirement": { + "status": "required" + }, + "social_security_survivors": { + "status": "required" + }, + "spm_unit_energy_subsidy": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM energy-subsidy (LIHEAP) resource input; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "spm_unit_pre_subsidy_childcare_expenses": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM childcare-expense input; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "spm_unit_tenure_type": { + "issue": "PolicyEngine/populace#32", + "reason": "HUD housing-assistance / tenure SPM input feeding capped housing subsidy; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "ssn_card_type": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "sstb_self_employment_income_before_lsr": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "sstb_self_employment_income_would_be_qualified": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "sstb_unadjusted_basis_qualified_property": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "sstb_w2_wages_from_qualified_business": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "state_fips": { + "status": "required" + }, + "stock_assets": { + "note": "SSI countable-resource asset input; required with NO reviewed exclusion per PolicyEngine/populace#368 so the gate fails until the asset stage is restored (Deliverable 2). Currently absent — this is the intended red gate.", + "status": "required" + }, + "student_loan_interest": { + "status": "required" + }, + "survivor_benefits": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "takes_up_aca_if_eligible": { + "status": "required" + }, + "takes_up_dc_ptc": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_early_head_start_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_eitc": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_head_start_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_housing_assistance_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_medicaid_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_medicare_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_snap_if_eligible": { + "status": "required" + }, + "takes_up_ssi_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "takes_up_tanf_if_eligible": { + "issue": "PolicyEngine/populace#312", + "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", + "status": "reviewed_exclusion" + }, + "tax_exempt_interest_income": { + "status": "required" + }, + "tax_exempt_ira_distributions": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "tax_exempt_private_pension_income": { + "status": "required" + }, + "taxable_401k_distributions": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "taxable_403b_distributions": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "taxable_interest_income": { + "status": "required" + }, + "taxable_ira_distributions": { + "status": "required" + }, + "taxable_private_pension_income": { + "status": "required" + }, + "taxable_sep_distributions": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "tenure_type": { + "issue": "PolicyEngine/populace#32", + "reason": "HUD housing-assistance / tenure SPM input feeding capped housing subsidy; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "tip_income": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "tract_geoid": { + "issue": "PolicyEngine/populace#38", + "reason": "Fine-grain geography code carried by the incumbent but not on the small national candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "treasury_tipped_occupation_code": { + "issue": "PolicyEngine/populace#38", + "reason": "Residual US tax-input / income-source layer not yet sourced onto the candidate frame; tracked as remaining input-layer work in #38.", + "status": "reviewed_exclusion" + }, + "unadjusted_basis_qualified_property": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "unemployment_compensation": { + "status": "required" + }, + "unrecaptured_section_1250_gain": { + "issue": "PolicyEngine/populace#274", + "reason": "Capital-gains component detail (collectibles / unrecaptured 1250 / 4952 election / investment-interest) not yet split onto the candidate frame.", + "status": "reviewed_exclusion" + }, + "unreimbursed_business_employee_expenses": { + "issue": "PolicyEngine/populace#32", + "reason": "SPM MOOP / work-expense / deduction input (childcare-MOOP-work-expense family of #32); not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "veterans_benefits": { + "status": "required" + }, + "w2_wages_from_qualified_business": { + "issue": "PolicyEngine/populace#298", + "reason": "Section 199A QBI / passthrough-qualification input; not yet on the candidate frame (QBI base already off vs targets, #298).", + "status": "reviewed_exclusion" + }, + "weekly_hours_worked_before_lsr": { + "issue": "PolicyEngine/populace#242", + "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", + "status": "reviewed_exclusion" + }, + "weeks_unemployed": { + "issue": "PolicyEngine/populace#38", + "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", + "status": "reviewed_exclusion" + }, + "workers_compensation": { + "issue": "PolicyEngine/populace#32", + "reason": "Workers' compensation benefit SPM input; not yet on the candidate frame.", + "status": "reviewed_exclusion" + }, + "would_claim_wic": { + "issue": "PolicyEngine/populace#312", + "reason": "WIC take-up / eligibility-screen input the incumbent seeds; not yet produced by Populace (take-up family).", + "status": "reviewed_exclusion" + }, + "would_file_taxes_voluntarily": { + "issue": "PolicyEngine/populace#312", + "reason": "Voluntary-filing take-up flag the incumbent seeds; not yet produced by Populace (take-up family).", + "status": "reviewed_exclusion" + } + }, + "counts": { + "required": 58, + "reviewed_exclusion": 100, + "total": 158 + }, + "derivation": "Required surface = ecps_parity_reference.json populated layers (input columns the pinned, sha-verified reference eCPS populates). status='reviewed_exclusion' for ecps_parity_known_gaps.json entries (reason+issue from that register); EXCEPT the SSI countable-resource asset inputs (bank_account_assets, stock_assets, bond_assets), which are status='required' with NO exclusion per PolicyEngine/populace#368 so the gate fails on today's artifacts and asset restoration (Deliverable 2) turns it green. All other populated layers are 'required'. Regenerate with tools/build_us_release_input_coverage_manifest.py.", + "description": "Declared full-coverage contract for a US release: every input column the reference eCPS exports must be persisted as a key with non-default signal, or carry a reviewed exclusion. Enforced as a hard release gate (populace.build.us_runtime.release_input_coverage) that generalizes assert_required_us_release_source_columns from 5 columns to the full eCPS input surface.", + "issue": "PolicyEngine/populace#368", + "reference": { + "filename": "enhanced_cps_2024.h5", + "period": "2024", + "repo_id": "policyengine/policyengine-us-data", + "repo_type": "model", + "revision": "21280dca5995e978d706740a8a4b9b7860cfd7b6", + "sha256": "0a6b961ad363a421bde99f2c8e5d8f20370bcba45fd303050537a25bdd805b14", + "vintage": "2024" + }, + "reform_coverage_probes": [ + { + "binding_inputs": [ + "bank_account_assets", + "stock_assets", + "bond_assets" + ], + "budget_measure": "ssi", + "effect_direction": "reform_minus_baseline", + "id": "ssi_asset_limit_10k_20k", + "issue": "PolicyEngine/populace#356", + "min_abs_effect": 1000000000.0, + "name": "SSI asset limits raised to $10k individual / $20k couple", + "parameter_changes": { + "gov.ssa.ssi.eligibility.resources.limit.couple": { + "2024-01-01.2100-12-31": 20000 + }, + "gov.ssa.ssi.eligibility.resources.limit.individual": { + "2024-01-01.2100-12-31": 10000 + } + }, + "reason": "Raising the SSI resource limit is a pure relaxation that only changes who passes meets_ssi_resource_test, which compares ssi_countable_resources = bank_account_assets + stock_assets + bond_assets against the limit. With those asset inputs absent, countable resources are 0 for every record, everyone already passes the resource test, and raising the limit scores exactly $0. Dense-native reference: +$1.6B at $10k/$20k, ~+$16.1B with no limit (PolicyEngine/populace#356)." + } + ], + "schema_version": 1, + "ssi_countable_resource_assets": [ + "bank_account_assets", + "stock_assets", + "bond_assets" + ] +} diff --git a/tools/build_us_release_input_coverage_manifest.py b/tools/build_us_release_input_coverage_manifest.py new file mode 100644 index 00000000..de3542d6 --- /dev/null +++ b/tools/build_us_release_input_coverage_manifest.py @@ -0,0 +1,196 @@ +"""Regenerate the US release input-column coverage manifest (populace #368). + +The manifest is the declared column-coverage contract the release gate enforces: +every input column the reference eCPS exports must be persisted by a populace +release as a real key with non-default signal, or carry a reviewed exclusion. + +Derivation (fully from checked-in, sha-pinned facts — no transient artifact): + +- Required surface = every input-variable column the pinned reference eCPS + populates, i.e. the ``nonzero_shares`` keys of ``ecps_parity_reference.json`` + (computed once from the sha-verified ``enhanced_cps_2024.h5``; an input the + incumbent exports but leaves all-zero is not a coverage requirement, the same + rule the parity gate uses). +- Status per column: + * ``reviewed_exclusion`` — the column is a documented incumbent-parity gap + (an entry in ``ecps_parity_known_gaps.json``, carrying that register's + reason and tracking issue), so the current candidate does not populate it + yet. EXCEPT the SSI countable-resource asset inputs (below). + * ``required`` — every other populated layer, PLUS the SSI countable-resource + asset inputs. Per #368 ("it must be an actual gate"), the asset inputs get + NO exclusion even though they are currently absent, so the gate ships red + for today's artifacts and Deliverable 2 (asset restoration) turns it green. + +The SSI asset inputs are ``bank_account_assets``, ``stock_assets``, and +``bond_assets`` — the exact ``adds`` list of ``ssi_countable_resources``. With +them absent, countable resources are 0 for every record, so the SSI asset-limit +reform probe scores $0; that is the failure the gate exists to surface. + +Run: uv run python tools/build_us_release_input_coverage_manifest.py +It rewrites packages/populace-build/src/populace/build/us/ +release_input_coverage_manifest.json. A test asserts the committed file matches +this regeneration, so the manifest cannot silently drift from the pinned eCPS +surface or the parity register. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +US_PACKAGE_DIR = ( + Path(__file__).resolve().parents[1] + / "packages" + / "populace-build" + / "src" + / "populace" + / "build" + / "us" +) +MANIFEST_PATH = US_PACKAGE_DIR / "release_input_coverage_manifest.json" + +#: The ``adds`` list of PolicyEngine-US ``ssi_countable_resources``: the asset +#: leaves SSI eligibility keys on. Absent from the release, they zero countable +#: resources and the SSI asset-limit reform scores $0. Per #368 these ship as +#: hard requirements with NO reviewed exclusion. +SSI_COUNTABLE_RESOURCE_ASSETS = ( + "bank_account_assets", + "stock_assets", + "bond_assets", +) + +#: The single pinned reform-coverage probe: raising the SSI resource limit from +#: the 2024 statutory $2,000 individual / $3,000 couple to $10,000 / $20,000 is +#: a pure relaxation that binds only through ``ssi_countable_resources``. Nonzero +#: iff the asset inputs are restored. Dense-native reference magnitudes: +$1.6B +#: at $10k/$20k and ~+$16.1B with no limit (populace #356). ``min_abs_effect`` is +#: a floor far below the plausible effect but far above simulation noise, so a +#: structural $0 fails while a real (even conservative) score passes. +REFORM_COVERAGE_PROBES = [ + { + "id": "ssi_asset_limit_10k_20k", + "name": "SSI asset limits raised to $10k individual / $20k couple", + "parameter_changes": { + "gov.ssa.ssi.eligibility.resources.limit.individual": { + "2024-01-01.2100-12-31": 10_000 + }, + "gov.ssa.ssi.eligibility.resources.limit.couple": { + "2024-01-01.2100-12-31": 20_000 + }, + }, + "budget_measure": "ssi", + "effect_direction": "reform_minus_baseline", + "binding_inputs": list(SSI_COUNTABLE_RESOURCE_ASSETS), + "min_abs_effect": 1_000_000_000.0, + "reason": ( + "Raising the SSI resource limit is a pure relaxation that only " + "changes who passes meets_ssi_resource_test, which compares " + "ssi_countable_resources = bank_account_assets + stock_assets + " + "bond_assets against the limit. With those asset inputs absent, " + "countable resources are 0 for every record, everyone already " + "passes the resource test, and raising the limit scores exactly " + "$0. Dense-native reference: +$1.6B at $10k/$20k, ~+$16.1B with no " + "limit (PolicyEngine/populace#356)." + ), + "issue": "PolicyEngine/populace#356", + } +] + + +def _load(name: str) -> dict: + return json.loads((US_PACKAGE_DIR / name).read_text(encoding="utf-8")) + + +def build_manifest() -> dict: + parity = _load("ecps_parity_reference.json") + known_gaps = _load("ecps_parity_known_gaps.json")["known_gaps"] + + populated_layers = { + name for name, share in parity["nonzero_shares"].items() if float(share) > 0.0 + } + ssi_assets = set(SSI_COUNTABLE_RESOURCE_ASSETS) + + missing_assets = sorted(ssi_assets - populated_layers) + if missing_assets: + raise ValueError( + "SSI countable-resource asset inputs are not in the reference eCPS " + f"populated surface, cannot pin them as required: {missing_assets}." + ) + + columns: dict[str, dict] = {} + for name in sorted(populated_layers): + if name in known_gaps and name not in ssi_assets: + entry = known_gaps[name] + columns[name] = { + "status": "reviewed_exclusion", + "reason": str(entry["reason"]), + "issue": str(entry["issue"]), + } + else: + column = {"status": "required"} + if name in ssi_assets: + # These are currently absent; #368 forbids an exclusion so the + # gate fails until Build J restores them. + column["note"] = ( + "SSI countable-resource asset input; required with NO " + "reviewed exclusion per PolicyEngine/populace#368 so the " + "gate fails until the asset stage is restored (Deliverable " + "2). Currently absent — this is the intended red gate." + ) + columns[name] = column + + required = sorted(n for n, c in columns.items() if c["status"] == "required") + reviewed = sorted( + n for n, c in columns.items() if c["status"] == "reviewed_exclusion" + ) + + return { + "schema_version": 1, + "issue": "PolicyEngine/populace#368", + "description": ( + "Declared full-coverage contract for a US release: every input " + "column the reference eCPS exports must be persisted as a key with " + "non-default signal, or carry a reviewed exclusion. Enforced as a " + "hard release gate (populace.build.us_runtime.release_input_" + "coverage) that generalizes assert_required_us_release_source_" + "columns from 5 columns to the full eCPS input surface." + ), + "reference": dict(parity["source"]), + "derivation": ( + "Required surface = ecps_parity_reference.json populated layers " + "(input columns the pinned, sha-verified reference eCPS populates). " + "status='reviewed_exclusion' for ecps_parity_known_gaps.json entries " + "(reason+issue from that register); EXCEPT the SSI countable-resource " + "asset inputs (bank_account_assets, stock_assets, bond_assets), which " + "are status='required' with NO exclusion per PolicyEngine/populace#368 " + "so the gate fails on today's artifacts and asset restoration " + "(Deliverable 2) turns it green. All other populated layers are " + "'required'. Regenerate with " + "tools/build_us_release_input_coverage_manifest.py." + ), + "counts": { + "required": len(required), + "reviewed_exclusion": len(reviewed), + "total": len(columns), + }, + "ssi_countable_resource_assets": list(SSI_COUNTABLE_RESOURCE_ASSETS), + "columns": columns, + "reform_coverage_probes": REFORM_COVERAGE_PROBES, + } + + +def main() -> None: + manifest = build_manifest() + MANIFEST_PATH.write_text( + json.dumps(manifest, indent=1, sort_keys=True, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + print( + f"wrote {MANIFEST_PATH} — {manifest['counts']['required']} required, " + f"{manifest['counts']['reviewed_exclusion']} reviewed exclusions, " + f"{len(manifest['reform_coverage_probes'])} reform probe(s)." + ) + + +if __name__ == "__main__": + main() From fe075d41cb3a3cd721f651df3c9bea4eff23d559 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 09:15:54 -0400 Subject: [PATCH 2/6] Add release input-coverage loader/gate + reform-coverage smoke modules (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release_input_coverage.py: manifest loader, us_release_input_coverage_gate over an export frame, and assert_release_input_coverage_manifest_current anti-rot (manifest must equal the pinned eCPS surface; SSI assets must stay required; every declared column a live PE-US input leaf). reform_coverage_smoke.py: us_reform_coverage_smoke_gate — a pinned bound reform scoring ~$0 fails the release. First probe: SSI $10k/$20k asset limits. Registered in us_runtime __init__ and country_package.json resources. Co-Authored-By: Claude Fable 5 --- .../populace/build/us/country_package.json | 1 + .../src/populace/build/us_runtime/__init__.py | 28 + .../build/us_runtime/reform_coverage_smoke.py | 129 +++++ .../us_runtime/release_input_coverage.py | 516 ++++++++++++++++++ 4 files changed, 674 insertions(+) create mode 100644 packages/populace-build/src/populace/build/us_runtime/reform_coverage_smoke.py create mode 100644 packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py diff --git a/packages/populace-build/src/populace/build/us/country_package.json b/packages/populace-build/src/populace/build/us/country_package.json index 10756d8e..252887ea 100644 --- a/packages/populace-build/src/populace/build/us/country_package.json +++ b/packages/populace-build/src/populace/build/us/country_package.json @@ -9,6 +9,7 @@ "fiscal_target_references.json", "obbba_reforms.json", "puf_aggregate_record_disaggregation.json", + "release_input_coverage_manifest.json", "soca_capital_gain_distribution_shares.json", "soi_baseline_levels.json", "source_stages.json", diff --git a/packages/populace-build/src/populace/build/us_runtime/__init__.py b/packages/populace-build/src/populace/build/us_runtime/__init__.py index 91ff5eb0..86624cc0 100644 --- a/packages/populace-build/src/populace/build/us_runtime/__init__.py +++ b/packages/populace-build/src/populace/build/us_runtime/__init__.py @@ -185,6 +185,9 @@ support_clone_index_column, support_source_id_column, ) +from populace.build.us_runtime.reform_coverage_smoke import ( + us_reform_coverage_smoke_gate, +) from populace.build.us_runtime.reform_validation import ( REFORM_VALIDATION_SCHEMA_VERSION, ReformValidationSpec, @@ -194,6 +197,19 @@ reform_validation_payload, write_reform_validation, ) +from populace.build.us_runtime.release_input_coverage import ( + SSI_COUNTABLE_RESOURCE_ASSETS, + US_RELEASE_INPUT_COVERAGE_RESOURCE, + ReformCoverageProbe, + ReleaseInputColumn, + ReleaseInputCoverageManifest, + assert_release_input_coverage_manifest_current, + load_release_input_coverage_manifest, + us_release_input_coverage_gate, + us_release_input_coverage_required_columns, + us_release_input_coverage_reviewed_exclusions, + us_release_reform_coverage_probes, +) from populace.build.us_runtime.snap_discretionary_exemption import ( US_SNAP_DISCRETIONARY_EXEMPTION_NONCONSTANT_PERSON_COLUMNS, US_SNAP_DISCRETIONARY_EXEMPTION_OUTPUT_COLUMN, @@ -430,6 +446,18 @@ "US_VALIDATION_PROVISION_INPUT_LEAVES", "ValidationInputLeaf", "assert_validation_leaf_registry_current", + "SSI_COUNTABLE_RESOURCE_ASSETS", + "US_RELEASE_INPUT_COVERAGE_RESOURCE", + "ReformCoverageProbe", + "ReleaseInputColumn", + "ReleaseInputCoverageManifest", + "assert_release_input_coverage_manifest_current", + "load_release_input_coverage_manifest", + "us_release_input_coverage_gate", + "us_release_input_coverage_required_columns", + "us_release_input_coverage_reviewed_exclusions", + "us_release_reform_coverage_probes", + "us_reform_coverage_smoke_gate", "write_us_source_coverage_diagnostics", "support_channel_column", "support_clone_index_column", diff --git a/packages/populace-build/src/populace/build/us_runtime/reform_coverage_smoke.py b/packages/populace-build/src/populace/build/us_runtime/reform_coverage_smoke.py new file mode 100644 index 00000000..053013b1 --- /dev/null +++ b/packages/populace-build/src/populace/build/us_runtime/reform_coverage_smoke.py @@ -0,0 +1,129 @@ +"""Reform-coverage smoke: a bound reform scoring $0 fails the release (#368). + +The column-coverage gate proves the required input *keys* are present and carry +signal. This is the end-to-end companion: a pinned set of reforms that +mechanically bind through named input leaves must move their budget measure on +the export. If a probe scores ~$0, the leaves it binds through are absent or +degenerate — the silent-zero failure proven through the engine, not just the +column surface. + +First probe: the SSI resource limit raised to $10k individual / $20k couple. It +binds only through ``ssi_countable_resources`` (= ``bank_account_assets`` + +``stock_assets`` + ``bond_assets``); with those absent, countable resources are +0 for every record, everyone already passes the resource test, and the +relaxation scores exactly $0. Until Deliverable 2 restores the asset stage this +probe fails by design — that is the gate doing its job. + +The gate takes an injected ``simulate(reform) -> simulation`` (the same seam as +:mod:`populace.build.us_runtime.reform_validation`), so it unit-tests without +policyengine-us and runs live against the written release H5 in the build via +:func:`populace.build.us_runtime.reform_validation.default_simulate_factory`. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from typing import Any + +from populace.build.gates import GateResult +from populace.build.us_runtime.release_input_coverage import ( + ReformCoverageProbe, + us_release_reform_coverage_probes, +) + +__all__ = [ + "us_reform_coverage_smoke_gate", +] + +# simulate(reform_or_None) -> object with .calculate(measure, period).sum() +SimulateFn = Callable[[Any], Any] + + +def _weighted_total(simulation: Any, measure: str, period: int) -> float: + """Weighted population total (policyengine-us MicroSeries .sum() is weighted).""" + return float(simulation.calculate(measure, period).sum()) + + +def _build_reform(parameter_changes: dict[str, Any]) -> Any: + from policyengine_core.reforms import Reform + + return Reform.from_dict(parameter_changes, country_id="us") + + +def us_reform_coverage_smoke_gate( + *, + simulate: SimulateFn, + probes: Iterable[ReformCoverageProbe] | None = None, + period: int = 2024, + name: str = "us_reform_coverage_smoke", +) -> GateResult: + """Score each pinned probe on the export; a ~$0 bound reform fails. + + For each probe, the budget-measure change (reform vs baseline, signed by + ``effect_direction``) must have magnitude at least ``min_abs_effect``. A + smaller magnitude means the reform did not bind — its ``binding_inputs`` are + absent/degenerate on the export — and fails the release. + + Args: + simulate: ``simulate(None)`` builds the baseline; ``simulate(reform)`` + builds the reformed simulation. Each result answers + ``.calculate(measure, period).sum()`` as a weighted total. + probes: Probes to run. Defaults to the shipped manifest's probe set. + period: The period to score at. + name: Gate name for the manifest. + + Returns: + The reform-coverage smoke gate result. Passes iff every probe scores at + least its ``min_abs_effect`` in magnitude. + """ + probes = tuple( + probes if probes is not None else us_release_reform_coverage_probes() + ) + if not probes: + raise ValueError( + "reform-coverage smoke gate needs at least one probe; a probe-less " + "gate would pass vacuously." + ) + + baseline = simulate(None) + failures: list[str] = [] + results: dict[str, Any] = {} + for probe in probes: + reform = _build_reform(dict(probe.parameter_changes)) + reformed = simulate(reform) + baseline_total = _weighted_total(baseline, probe.budget_measure, period) + reform_total = _weighted_total(reformed, probe.budget_measure, period) + if probe.effect_direction == "baseline_minus_reform": + effect = baseline_total - reform_total + else: + effect = reform_total - baseline_total + results[probe.id] = { + "name": probe.name, + "budget_measure": probe.budget_measure, + "baseline_total": baseline_total, + "reform_total": reform_total, + "effect": effect, + "min_abs_effect": probe.min_abs_effect, + "binding_inputs": list(probe.binding_inputs), + "issue": probe.issue, + "passed": abs(effect) >= probe.min_abs_effect, + } + if abs(effect) < probe.min_abs_effect: + failures.append( + f"{probe.id}: '{probe.name}' scores {effect:+,.0f} on " + f"{probe.budget_measure} (|effect| < ${probe.min_abs_effect:,.0f}) " + "— the reform did not bind, so its input leaves " + f"{list(probe.binding_inputs)} are absent or degenerate on the " + f"export. {probe.reason} Restore them ({probe.issue})." + ) + + return GateResult( + name=name, + passed=not failures, + failures=tuple(failures), + details={ + "period": int(period), + "probes": len(probes), + "results": results, + }, + ) diff --git a/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py b/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py new file mode 100644 index 00000000..bfa96920 --- /dev/null +++ b/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py @@ -0,0 +1,516 @@ +"""US release input-column coverage: the eCPS export surface as a HARD gate. + +populace #368. The launch failure this closes: an SSI asset-limit reform scored +exactly $0 on a certified default because ``ssi_countable_resources`` is 0 for +every record — the asset input columns were dropped at the base pool and are +absent from the export. This is the #340/#356/#361/#278 family: input columns +the reference eCPS exports that the sparse national release never persists, so +the engine silently defaults them and every reform binding through them scores +$0 while export-mass parity (a ~35-column weighted-mass check) still passes. +Column *mass* parity is not column *coverage*. + +This module is the coverage contract. It declares, in a versioned in-repo +manifest, every input column the pinned reference eCPS populates, each with a +status: + +- ``required`` — the release must persist it as a key with non-default signal; +- ``reviewed_exclusion`` — a documented, issue-tracked gap allowed to be + absent/degenerate for now (the debt ledger that shrinks as inputs are + restored). + +:func:`us_release_input_coverage_gate` enforces it on the export frame and, wired +into the release tool, hard-fails the export before ``write_dataset`` exactly +like the export-mass parity gate. It generalizes +:func:`populace.build.us_runtime.l0_refit_export.assert_required_us_release_source_columns` +from five SNAP/ACA/immigration columns to the full eCPS input surface. + +Per #368 ("it must be an actual gate"), the three SSI countable-resource asset +inputs (``bank_account_assets``, ``stock_assets``, ``bond_assets``) are +``required`` with NO reviewed exclusion even though they are currently absent, so +the gate ships RED for today's artifacts; asset restoration (Deliverable 2) turns +it green. A gate that fails the current default is the point, not a bug. + +:func:`assert_release_input_coverage_manifest_current` proves the manifest against +the pinned eCPS surface and the live PolicyEngine-US graph, so the register +cannot silently rot: it must cover exactly the reference eCPS populated layers, +every declared column must be a real engine input leaf, and the SSI asset inputs +must stay hard requirements. +""" + +from __future__ import annotations + +import json +from collections.abc import Iterable, Mapping +from dataclasses import dataclass, field +from importlib.resources import files +from pathlib import Path +from typing import Any + +from populace.build.gates import GateResult, input_column_coverage_gate + +__all__ = [ + "US_RELEASE_INPUT_COVERAGE_RESOURCE", + "ReformCoverageProbe", + "ReleaseInputColumn", + "ReleaseInputCoverageManifest", + "assert_release_input_coverage_manifest_current", + "load_release_input_coverage_manifest", + "us_release_input_coverage_gate", + "us_release_input_coverage_required_columns", + "us_release_input_coverage_reviewed_exclusions", + "us_release_reform_coverage_probes", +] + +US_RELEASE_INPUT_COVERAGE_RESOURCE = "release_input_coverage_manifest.json" + +_US_PACKAGE = "populace.build.us" +_ECPS_PARITY_REFERENCE_RESOURCE = "ecps_parity_reference.json" + +REQUIRED_STATUS = "required" +REVIEWED_EXCLUSION_STATUS = "reviewed_exclusion" +_VALID_STATUSES = frozenset({REQUIRED_STATUS, REVIEWED_EXCLUSION_STATUS}) + +#: The ``adds`` list of PolicyEngine-US ``ssi_countable_resources``: the asset +#: leaves SSI eligibility keys on. Per #368 these ship as hard requirements with +#: NO reviewed exclusion, so the coverage gate and the SSI reform probe both fail +#: until the asset stage is restored. +SSI_COUNTABLE_RESOURCE_ASSETS = ( + "bank_account_assets", + "stock_assets", + "bond_assets", +) + + +@dataclass(frozen=True) +class ReleaseInputColumn: + """One declared coverage-contract column. + + Attributes: + name: The PolicyEngine-US input variable the release must persist. + status: ``"required"`` (present + non-degenerate) or + ``"reviewed_exclusion"`` (a tracked gap allowed to be absent). + reason: Why the gap is accepted — required and non-empty for a reviewed + exclusion, empty for a hard requirement. + issue: The tracking issue owning the gap's closure — required for a + reviewed exclusion. + note: Optional free-text annotation (e.g. why an absent column is still + a hard requirement). + """ + + name: str + status: str + reason: str = "" + issue: str = "" + note: str = "" + + def __post_init__(self) -> None: + if not self.name: + raise ValueError("ReleaseInputColumn.name is required.") + if self.status not in _VALID_STATUSES: + raise ValueError( + f"{self.name}: status must be one of {sorted(_VALID_STATUSES)}, " + f"got {self.status!r}." + ) + if self.status == REVIEWED_EXCLUSION_STATUS: + if not self.reason: + raise ValueError( + f"{self.name}: a reviewed exclusion needs a reason " + "(an undocumented exclusion is a silent zero with extra steps)." + ) + if not self.issue: + raise ValueError( + f"{self.name}: a reviewed exclusion needs a tracking issue." + ) + + +@dataclass(frozen=True) +class ReformCoverageProbe: + """A pinned reform whose $0 score on the export signals a coverage hole. + + A reform that mechanically binds through named input leaves must move its + budget measure. If it scores ~$0 on the release, the leaves it binds through + are absent/degenerate — the same silent-zero failure the column gate catches, + proven end-to-end against the engine. + + Attributes: + id: Stable probe id. + name: Human-readable reform description. + parameter_changes: ``Reform.from_dict`` payload (country ``us``). + budget_measure: The variable whose weighted total change is scored. + effect_direction: ``"reform_minus_baseline"`` or + ``"baseline_minus_reform"``. + binding_inputs: The input leaves the reform binds through; named in the + failure so the fix target is explicit. + min_abs_effect: The reform fails the smoke gate when + ``abs(effect) < min_abs_effect`` — a floor above simulation noise + and far below the plausible effect, so only a structural zero fails. + reason: Why a $0 here means a coverage hole (the mechanism). + issue: Tracking issue for the restoration work. + """ + + id: str + name: str + parameter_changes: Mapping[str, Any] + budget_measure: str + binding_inputs: tuple[str, ...] + min_abs_effect: float + reason: str + issue: str + effect_direction: str = "reform_minus_baseline" + + def __post_init__(self) -> None: + if not self.id: + raise ValueError("ReformCoverageProbe.id is required.") + if not self.parameter_changes: + raise ValueError(f"{self.id}: parameter_changes is required.") + if not self.budget_measure: + raise ValueError(f"{self.id}: budget_measure is required.") + if self.effect_direction not in { + "reform_minus_baseline", + "baseline_minus_reform", + }: + raise ValueError( + f"{self.id}: effect_direction must be 'reform_minus_baseline' or " + "'baseline_minus_reform'." + ) + if not (self.min_abs_effect > 0): + raise ValueError(f"{self.id}: min_abs_effect must be positive.") + + +@dataclass(frozen=True) +class ReleaseInputCoverageManifest: + """The full parsed coverage contract. + + Attributes: + reference: Provenance of the reference eCPS the surface is derived from. + columns: Every declared column, in name order. + probes: The pinned reform-coverage probes. + schema_version: Manifest schema version. + """ + + reference: Mapping[str, str] + columns: tuple[ReleaseInputColumn, ...] + probes: tuple[ReformCoverageProbe, ...] = () + schema_version: int = 1 + _by_name: dict[str, ReleaseInputColumn] = field( + default_factory=dict, repr=False, compare=False + ) + + def __post_init__(self) -> None: + by_name: dict[str, ReleaseInputColumn] = {} + for column in self.columns: + if column.name in by_name: + raise ValueError(f"Duplicate manifest column {column.name!r}.") + by_name[column.name] = column + object.__setattr__(self, "_by_name", by_name) + + @property + def declared_columns(self) -> frozenset[str]: + """Every column the manifest declares (required + reviewed).""" + return frozenset(self._by_name) + + @property + def required_columns(self) -> frozenset[str]: + """Columns that must be present and non-degenerate.""" + return frozenset( + column.name + for column in self.columns + if column.status == REQUIRED_STATUS + ) + + @property + def reviewed_exclusions(self) -> dict[str, str]: + """Reviewed-exclusion columns as ``name -> reason``.""" + return { + column.name: column.reason + for column in self.columns + if column.status == REVIEWED_EXCLUSION_STATUS + } + + +def _resource_text(resource: str) -> str: + candidate = Path(resource) + if candidate.exists(): + return candidate.read_text(encoding="utf-8") + return files(_US_PACKAGE).joinpath(resource).read_text(encoding="utf-8") + + +def _resource_payload(resource: str) -> Mapping[str, Any]: + raw = json.loads(_resource_text(resource)) + if not isinstance(raw, Mapping): + raise ValueError(f"{resource}: expected a JSON object.") + return raw + + +def load_release_input_coverage_manifest( + resource: str = US_RELEASE_INPUT_COVERAGE_RESOURCE, +) -> ReleaseInputCoverageManifest: + """Load and validate the release input-column coverage manifest. + + Raises: + ValueError: If the payload shape is wrong, a column has an unknown + status, a reviewed exclusion is missing its reason or issue, or the + declared column set is empty (a silently-empty manifest would make + the coverage gate vacuous). + """ + payload = _resource_payload(resource) + + raw_reference = payload.get("reference") + if not isinstance(raw_reference, Mapping): + raise ValueError(f"{resource}: 'reference' must be a JSON object.") + reference = {str(key): str(value) for key, value in raw_reference.items()} + + raw_columns = payload.get("columns") + if not isinstance(raw_columns, Mapping) or not raw_columns: + raise ValueError( + f"{resource}: 'columns' must be a non-empty JSON object; a silently " + "empty manifest would make the coverage gate vacuous." + ) + columns: list[ReleaseInputColumn] = [] + for name, entry in sorted(raw_columns.items()): + if not isinstance(entry, Mapping): + raise ValueError(f"{resource}: column {name!r} must be a JSON object.") + columns.append( + ReleaseInputColumn( + name=str(name), + status=str(entry.get("status", "")), + reason=str(entry.get("reason", "")), + issue=str(entry.get("issue", "")), + note=str(entry.get("note", "")), + ) + ) + + probes: list[ReformCoverageProbe] = [] + for raw_probe in payload.get("reform_coverage_probes", []) or []: + if not isinstance(raw_probe, Mapping): + raise ValueError(f"{resource}: each reform probe must be a JSON object.") + parameter_changes = raw_probe.get("parameter_changes") + if not isinstance(parameter_changes, Mapping): + raise ValueError( + f"{resource}: probe {raw_probe.get('id')!r} parameter_changes " + "must be a JSON object." + ) + probes.append( + ReformCoverageProbe( + id=str(raw_probe.get("id", "")), + name=str(raw_probe.get("name", "")), + parameter_changes=dict(parameter_changes), + budget_measure=str(raw_probe.get("budget_measure", "")), + binding_inputs=tuple( + str(leaf) for leaf in raw_probe.get("binding_inputs", ()) + ), + min_abs_effect=float(raw_probe.get("min_abs_effect", 0.0)), + reason=str(raw_probe.get("reason", "")), + issue=str(raw_probe.get("issue", "")), + effect_direction=str( + raw_probe.get("effect_direction", "reform_minus_baseline") + ), + ) + ) + + schema_version = payload.get("schema_version", 1) + if not isinstance(schema_version, int): + raise ValueError(f"{resource}: 'schema_version' must be an integer.") + + return ReleaseInputCoverageManifest( + reference=reference, + columns=tuple(columns), + probes=tuple(probes), + schema_version=schema_version, + ) + + +def us_release_input_coverage_required_columns() -> frozenset[str]: + """The hard-required input columns from the shipped manifest.""" + return load_release_input_coverage_manifest().required_columns + + +def us_release_input_coverage_reviewed_exclusions() -> dict[str, str]: + """The reviewed-exclusion register from the shipped manifest.""" + return load_release_input_coverage_manifest().reviewed_exclusions + + +def us_release_reform_coverage_probes() -> tuple[ReformCoverageProbe, ...]: + """The pinned reform-coverage probes from the shipped manifest.""" + return load_release_input_coverage_manifest().probes + + +def _degenerate_columns( + column_values: Mapping[str, Any], + engine: Any, +) -> set[str]: + """Columns whose every observed value equals the engine default. + + Reuses :func:`populace.build.gates.default_valued_columns_gate`'s exact + degeneracy classification (same scalar/enum comparison the degenerate-input + gate uses) so "present but default" is judged identically everywhere. + """ + from populace.build.gates import default_valued_columns_gate + + defaults = engine.default_values(sorted(column_values)) + classification = default_valued_columns_gate(column_values, defaults) + return set(classification.details["default_valued_columns"]) + + +def us_release_input_coverage_gate( + frame: Any, + engine: Any, + *, + manifest: ReleaseInputCoverageManifest | None = None, +) -> GateResult: + """Build the named US release input-column coverage gate for an export frame. + + Every ``required`` manifest column must be persisted by ``frame`` as a key + with at least one non-default value; a required column that is absent or + degenerate (every value the engine default) fails the gate, and a reviewed + exclusion whose column now carries signal is stale and fails too (#286 + cannot-rot). Run on the calibrated export frame just before + ``write_dataset``, it hard-fails the release like the export-mass parity gate. + + Args: + frame: The export :class:`populace.frame.Frame`. + engine: A ``PolicyEngineUSEngine`` (for input-variable defaults). + manifest: Override the shipped manifest (tests). + + Returns: + The ``"us_release_input_coverage"`` gate result. + """ + manifest = manifest or load_release_input_coverage_manifest() + required = manifest.required_columns + reviewed = manifest.reviewed_exclusions + relevant = required | set(reviewed) + + present_values: dict[str, Any] = {} + for entity in frame.entities: + table = frame.table(entity) + for column in table.columns: + if column in relevant and column not in present_values: + present_values[column] = table[column].to_numpy() + + degenerate = _degenerate_columns(present_values, engine) + return input_column_coverage_gate( + present_values.keys(), + required_columns=required, + degenerate_columns=degenerate, + reviewed_exclusions=reviewed, + name="us_release_input_coverage", + ) + + +def _coverage_engine() -> Any | None: + """A PolicyEngine-US adapter for graph checks, or ``None`` if absent. + + Mirrors the validation-leaf registry's lazy engine: succeeds without the + ``[us]`` extra and treats a missing extra at call time as ``None``, so the + manifest consistency check is a no-op off-build and runs live where the + extra is installed (the build). + """ + try: + from populace.frame.adapters.policyengine_us import PolicyEngineUSEngine + except ImportError: + return None + try: + return PolicyEngineUSEngine() + except Exception: # pragma: no cover - defensive + return None + + +def _ecps_populated_layers() -> frozenset[str]: + payload = _resource_payload(_ECPS_PARITY_REFERENCE_RESOURCE) + shares = payload.get("nonzero_shares") + if not isinstance(shares, Mapping) or not shares: + raise ValueError( + f"{_ECPS_PARITY_REFERENCE_RESOURCE}: 'nonzero_shares' must be a " + "non-empty JSON object." + ) + return frozenset( + str(name) for name, share in shares.items() if float(share) > 0.0 + ) + + +def assert_release_input_coverage_manifest_current( + *, + engine: Any | None = None, + manifest: ReleaseInputCoverageManifest | None = None, +) -> None: + """Fail if the coverage manifest has drifted from its authoritative sources. + + Checked against the checked-in facts (always) and the live engine graph + (when available): + + - The declared columns must equal the reference eCPS populated input surface + (``ecps_parity_reference.json``): the manifest is the eCPS export surface, + no more, no less. A layer the incumbent gains/loses must be reflected here. + - The three SSI countable-resource asset inputs must be ``required`` with no + reviewed exclusion — the #368 red-gate guarantee cannot be quietly undone. + - Every declared column must be a real PolicyEngine-US input leaf, and every + probe's ``binding_inputs`` / ``budget_measure`` must resolve on the live + engine, so the contract cannot guard names the engine no longer has. + + A no-op for the engine-graph half when no engine is available (the workspace + test environment); the checked-in-facts half always runs. + + Raises: + ValueError: Naming every drift found. + """ + manifest = manifest or load_release_input_coverage_manifest() + failures: list[str] = [] + + declared = set(manifest.declared_columns) + surface = set(_ecps_populated_layers()) + missing_from_manifest = sorted(surface - declared) + extra_in_manifest = sorted(declared - surface) + if missing_from_manifest: + failures.append( + "manifest is missing reference eCPS populated input column(s) " + f"{missing_from_manifest}; regenerate with " + "tools/build_us_release_input_coverage_manifest.py." + ) + if extra_in_manifest: + failures.append( + "manifest declares column(s) the reference eCPS does not populate " + f"{extra_in_manifest}; regenerate the manifest." + ) + + required = manifest.required_columns + reviewed = set(manifest.reviewed_exclusions) + for asset in SSI_COUNTABLE_RESOURCE_ASSETS: + if asset in reviewed: + failures.append( + f"{asset}: SSI countable-resource asset input is a reviewed " + "exclusion, but #368 requires it be a hard requirement with no " + "exclusion so the gate fails until the asset stage is restored." + ) + elif asset not in required: + failures.append( + f"{asset}: SSI countable-resource asset input must be a " + "required manifest column (#368)." + ) + + if engine is None: + engine = _coverage_engine() + if engine is not None: + try: + input_variables = set(engine.variables()) + except ImportError: + input_variables = None + if input_variables is not None: + non_leaves = sorted(declared - input_variables) + if non_leaves: + failures.append( + "manifest declares column(s) that are not PolicyEngine-US " + f"input leaves (formula-owned or unknown): {non_leaves}." + ) + for probe in manifest.probes: + bad_inputs = sorted(set(probe.binding_inputs) - input_variables) + if bad_inputs: + failures.append( + f"probe {probe.id!r}: binding_inputs are not " + f"PolicyEngine-US input leaves: {bad_inputs}." + ) + + if failures: + raise ValueError( + "US release input-column coverage manifest has drifted:\n" + + "\n".join(f" - {line}" for line in failures) + ) From f8b1b754de899eaec26764fbe78aca6996d9e656 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 09:44:00 -0400 Subject: [PATCH 3/6] Wire the input-coverage + reform-coverage smoke gates into the US release tool (#368) Complete Deliverable 1's release wiring: the coverage gate and reform-coverage smoke now run in tools/build_us_fiscal_refresh_release.py on BOTH the dense and sparse default paths, aborting the release exactly like the export-mass parity gate. - Preflight: assert_release_input_coverage_manifest_current() runs before the expensive calibration, so a manifest that has drifted from the pinned eCPS surface (or quietly demoted the SSI assets) fails fast. - After the calibrated export frame is built and before write_dataset: us_release_input_coverage_gate scores every required eCPS input column for presence + non-degenerate signal, writes input_coverage.json, and raises on failure (--allow-input-coverage-gaps is a diagnostic-only escape hatch; release builds leave it unset). - After write_dataset: us_reform_coverage_smoke_gate scores the pinned bound reforms on the written H5 (first probe: SSI asset limits $10k/$20k), writes reform_coverage_smoke.json, and raises on a ~$0 bound reform. The SSI countable-resource assets ship as hard requirements with no reviewed exclusion, so both gates are RED on today's asset-less artifacts by design; Deliverable 2 (asset-stage restoration) turns them green. Also re-export input_column_coverage_gate from populace.build to match every sibling gate, and drop an unused import the loader carried. Co-Authored-By: Claude Fable 5 --- .../src/populace/build/__init__.py | 2 + .../us_runtime/release_input_coverage.py | 10 +- tools/build_us_fiscal_refresh_release.py | 150 +++++++++++++++++- 3 files changed, 154 insertions(+), 8 deletions(-) diff --git a/packages/populace-build/src/populace/build/__init__.py b/packages/populace-build/src/populace/build/__init__.py index 4ad0f764..12803967 100644 --- a/packages/populace-build/src/populace/build/__init__.py +++ b/packages/populace-build/src/populace/build/__init__.py @@ -70,6 +70,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None: export_surface_gate, exported_nonzero_gate, formula_owned_export_gate, + input_column_coverage_gate, input_mass_parity_gate, macro_realism_gate, nonconstant_columns_gate, @@ -155,6 +156,7 @@ def _assert_frame_compatible(version: str, required: tuple[int, int]) -> None: "export_surface_gate", "exported_nonzero_gate", "formula_owned_export_gate", + "input_column_coverage_gate", "input_mass_parity_gate", "load_ledger_consumer_artifact", "macro_realism_gate", diff --git a/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py b/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py index bfa96920..9123ee99 100644 --- a/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py +++ b/packages/populace-build/src/populace/build/us_runtime/release_input_coverage.py @@ -40,7 +40,7 @@ from __future__ import annotations import json -from collections.abc import Iterable, Mapping +from collections.abc import Mapping from dataclasses import dataclass, field from importlib.resources import files from pathlib import Path @@ -213,9 +213,7 @@ def declared_columns(self) -> frozenset[str]: def required_columns(self) -> frozenset[str]: """Columns that must be present and non-degenerate.""" return frozenset( - column.name - for column in self.columns - if column.status == REQUIRED_STATUS + column.name for column in self.columns if column.status == REQUIRED_STATUS ) @property @@ -423,9 +421,7 @@ def _ecps_populated_layers() -> frozenset[str]: f"{_ECPS_PARITY_REFERENCE_RESOURCE}: 'nonzero_shares' must be a " "non-empty JSON object." ) - return frozenset( - str(name) for name, share in shares.items() if float(share) > 0.0 - ) + return frozenset(str(name) for name, share in shares.items() if float(share) > 0.0) def assert_release_input_coverage_manifest_current( diff --git a/tools/build_us_fiscal_refresh_release.py b/tools/build_us_fiscal_refresh_release.py index 57ea2044..425b310e 100644 --- a/tools/build_us_fiscal_refresh_release.py +++ b/tools/build_us_fiscal_refresh_release.py @@ -51,6 +51,7 @@ US_FISCAL_TARGET_SUPPORT_EXCLUSIONS, US_JCT_TAX_EXPENDITURE_REFORMS, US_SOURCE_MANIFEST, + assert_release_input_coverage_manifest_current, assert_validation_leaf_registry_current, compile_us_fiscal_target_registry, hard_target_package_aliases, @@ -59,6 +60,8 @@ us_hours_worked_signal_gate, us_immigration_composition_gate, us_pregnancy_signal_gate, + us_reform_coverage_smoke_gate, + us_release_input_coverage_gate, us_snap_discretionary_exemption_signal_gate, us_snap_take_up_signal_gate, us_source_coverage_diagnostics, @@ -686,6 +689,36 @@ def _parse_args() -> argparse.Namespace: "unexempted) without failing the build." ), ) + parser.add_argument( + "--allow-input-coverage-gaps", + action="store_true", + help=( + "Diagnostic escape hatch (populace#368): record the release " + "input-column coverage gate result — required eCPS input columns " + "the export drops or leaves degenerate — without failing the " + "build. Release builds must leave this unset; the gate is a hard " + "certification blocker." + ), + ) + parser.add_argument( + "--skip-reform-coverage-smoke", + action="store_true", + help=( + "Do not run the reform-coverage smoke gate (populace#368): the " + "pinned bound-reform probes (SSI $10k/$20k asset limits) that must " + "score nonzero on the written release. Skipping loses the " + "end-to-end $0-reform backstop; release builds should leave it on." + ), + ) + parser.add_argument( + "--allow-reform-coverage-smoke-failures", + action="store_true", + help=( + "Diagnostic escape hatch (populace#368): record the reform-coverage " + "smoke gate result without failing the build when a bound reform " + "probe scores ~$0. Release builds must leave this unset." + ), + ) parser.add_argument( "--epochs", type=int, @@ -5372,6 +5405,12 @@ def main() -> None: for failure in validation_input_coverage_gate.failures ) ) + # Preflight (populace#368): the release input-column coverage manifest must + # still equal the pinned reference eCPS input surface, keep the SSI + # countable-resource assets as hard requirements, and declare only live + # PolicyEngine-US input leaves. A drifted manifest fails here before the + # expensive calibration so a stale contract cannot silently narrow coverage. + assert_release_input_coverage_manifest_current() ledger_artifact = load_ledger_consumer_artifact( args.ledger_facts, expected_facts_sha256=args.ledger_facts_sha256, @@ -6082,6 +6121,56 @@ def main() -> None: if args.dense_default_dataset else _with_l0_refit_weights(base_frame, result) ) + release_engine = PolicyEngineUSEngine() + # populace#368: full eCPS input-column coverage as a HARD release gate. + # Every input column the reference eCPS exports must be persisted by the + # export as a key with non-default signal, or carry a reviewed exclusion. + # This generalizes assert_required_us_release_source_columns (5 SNAP/ACA/ + # immigration columns) to the whole eCPS input surface and closes the hole + # export-mass parity leaves open: an input the base never imputed (the SSI + # countable-resource assets, tips, education credits, ...) is absent from the + # export entirely, so every reform binding through it silently scores ~$0 + # while mass and parity gates still pass. Runs on the calibrated export for + # BOTH the dense and sparse default paths (both reach this line). The SSI + # asset inputs ship as hard requirements with no exclusion, so this gate is + # RED on today's asset-less artifacts by design (Deliverable 2 turns it + # green); that is the gate doing its job, not a bug. + input_coverage_gate = us_release_input_coverage_gate(export_frame, release_engine) + input_coverage_path = release_dir / "input_coverage.json" + input_coverage_path.write_text( + json.dumps( + { + "schema_version": 1, + "enforced": not args.allow_input_coverage_gaps, + "input_coverage": { + "passed": input_coverage_gate.passed, + "failures": list(input_coverage_gate.failures), + "details": dict(input_coverage_gate.details), + }, + }, + indent=2, + sort_keys=True, + ) + + "\n" + ) + if telemetry is not None: + telemetry.attach_artifact("input_coverage", input_coverage_path) + if not input_coverage_gate.passed and not args.allow_input_coverage_gaps: + if telemetry is not None: + telemetry.stage( + "export_dataset", + status="failed", + message="Release input-column coverage gate failed.", + failures=list(input_coverage_gate.failures), + force_upload=True, + ) + raise RuntimeError( + "Release gates failed: " + + "; ".join( + f"Input coverage failed: {failure}" + for failure in input_coverage_gate.failures + ) + ) # #327: the export gate compares the calibrated export against a reference. # By default that reference is the raw pre-calibration base — but for a dense # parent built from a raw pooled-ASEC base, calibration correctly scales @@ -6161,7 +6250,66 @@ def main() -> None: ) ) dataset_path = artifact_root / DATASET_FILENAME - PolicyEngineUSEngine().write_dataset(export_frame, dataset_path, period=PERIOD) + release_engine.write_dataset(export_frame, dataset_path, period=PERIOD) + # populace#368: reform-coverage smoke on the WRITTEN release H5. The column + # gate above proves the required keys exist and carry signal; this is the + # end-to-end backstop: each pinned probe (first: SSI asset limits at + # $10k/$20k) mechanically binds through named input leaves and must move + # its budget measure. A ~$0 score on a bound reform means those leaves are + # absent or degenerate — the release aborts before any manifest or + # contract file certifies the artifact. Runs on both dense and sparse + # default paths (both reach this line). Until the asset stage is restored + # (Deliverable 2) the SSI probe fails by design; that is the gate working. + if not args.skip_reform_coverage_smoke: + if telemetry is not None: + telemetry.stage( + "reform_coverage_smoke", + message="Scoring pinned bound-reform probes on the written H5.", + ) + reform_coverage_smoke_gate = us_reform_coverage_smoke_gate( + simulate=default_simulate_factory(dataset_path), + period=PERIOD, + ) + reform_coverage_smoke_path = release_dir / "reform_coverage_smoke.json" + reform_coverage_smoke_path.write_text( + json.dumps( + { + "schema_version": 1, + "enforced": not args.allow_reform_coverage_smoke_failures, + "reform_coverage_smoke": { + "passed": reform_coverage_smoke_gate.passed, + "failures": list(reform_coverage_smoke_gate.failures), + "details": dict(reform_coverage_smoke_gate.details), + }, + }, + indent=2, + sort_keys=True, + ) + + "\n" + ) + if telemetry is not None: + telemetry.attach_artifact( + "reform_coverage_smoke", reform_coverage_smoke_path + ) + if ( + not reform_coverage_smoke_gate.passed + and not args.allow_reform_coverage_smoke_failures + ): + if telemetry is not None: + telemetry.stage( + "reform_coverage_smoke", + status="failed", + message="Reform-coverage smoke gate failed.", + failures=list(reform_coverage_smoke_gate.failures), + force_upload=True, + ) + raise RuntimeError( + "Release gates failed: " + + "; ".join( + f"Reform coverage smoke failed: {failure}" + for failure in reform_coverage_smoke_gate.failures + ) + ) if args.audit_export_targets: if telemetry is not None: telemetry.stage( From d39b71ac396871f8792191e49102886a40a21a58 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 09:44:09 -0400 Subject: [PATCH 4/6] Stop the coverage manifest naming the retired data package (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The generated coverage manifest copied the eCPS parity reference's full source block into its own "reference" field, which carried the retired data package's repo id. The manifest is not on the incumbent-reference allow-list, so that tripped the live-tree guard (test_us_plan.test_no_incumbent_data_package_ references_in_live_tree) and would have failed CI. Nothing reads the manifest's reference field — the gate and the anti-rot check derive the column surface from ecps_parity_reference.json directly — so the generator now records only the sha-locked coordinates that do not name a package (filename, content sha256, revision, vintage, period) plus a derived_from pointer to that allow-listed parity reference. Column set is unchanged (58 required incl. the 3 SSI assets, 100 reviewed exclusions). Co-Authored-By: Claude Fable 5 --- .../us/release_input_coverage_manifest.json | 4 +-- ...uild_us_release_input_coverage_manifest.py | 27 ++++++++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json index 7d329cdd..7184dd81 100644 --- a/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json +++ b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json @@ -687,10 +687,10 @@ "description": "Declared full-coverage contract for a US release: every input column the reference eCPS exports must be persisted as a key with non-default signal, or carry a reviewed exclusion. Enforced as a hard release gate (populace.build.us_runtime.release_input_coverage) that generalizes assert_required_us_release_source_columns from 5 columns to the full eCPS input surface.", "issue": "PolicyEngine/populace#368", "reference": { + "derived_from": "ecps_parity_reference.json", "filename": "enhanced_cps_2024.h5", + "note": "Column surface derived from the populated input layers of the pinned, sha-verified reference eCPS recorded in ecps_parity_reference.json (the launch parity contract, PolicyEngine/populace#313) — the single allow-listed record of the incumbent coordinates. This manifest names no data package.", "period": "2024", - "repo_id": "policyengine/policyengine-us-data", - "repo_type": "model", "revision": "21280dca5995e978d706740a8a4b9b7860cfd7b6", "sha256": "0a6b961ad363a421bde99f2c8e5d8f20370bcba45fd303050537a25bdd805b14", "vintage": "2024" diff --git a/tools/build_us_release_input_coverage_manifest.py b/tools/build_us_release_input_coverage_manifest.py index de3542d6..83bb1dfa 100644 --- a/tools/build_us_release_input_coverage_manifest.py +++ b/tools/build_us_release_input_coverage_manifest.py @@ -144,6 +144,31 @@ def build_manifest() -> dict: n for n, c in columns.items() if c["status"] == "reviewed_exclusion" ) + # Provenance without naming the retired data package. Only the eCPS parity + # reference, its loader, and its test are allow-listed to name the incumbent + # (test_us_plan.test_no_incumbent_data_package_references_in_live_tree); this + # manifest is not, so it records the sha-locked coordinates that DON'T name a + # package (filename + content sha + revision + vintage) and points at that + # allow-listed parity reference for the full record. Nothing reads these + # fields — the gate and the anti-rot check derive the surface from + # ecps_parity_reference.json directly — so this stays pure documentation. + parity_source = dict(parity["source"]) + reference = { + "derived_from": "ecps_parity_reference.json", + "filename": str(parity_source.get("filename", "")), + "revision": str(parity_source.get("revision", "")), + "sha256": str(parity_source.get("sha256", "")), + "vintage": str(parity_source.get("vintage", "")), + "period": str(parity_source.get("period", "")), + "note": ( + "Column surface derived from the populated input layers of the " + "pinned, sha-verified reference eCPS recorded in " + "ecps_parity_reference.json (the launch parity contract, " + "PolicyEngine/populace#313) — the single allow-listed record of the " + "incumbent coordinates. This manifest names no data package." + ), + } + return { "schema_version": 1, "issue": "PolicyEngine/populace#368", @@ -155,7 +180,7 @@ def build_manifest() -> dict: "coverage) that generalizes assert_required_us_release_source_" "columns from 5 columns to the full eCPS input surface." ), - "reference": dict(parity["source"]), + "reference": reference, "derivation": ( "Required surface = ecps_parity_reference.json populated layers " "(input columns the pinned, sha-verified reference eCPS populates). " From d94a7d7dba8f282aff33aba91ca1c627536c5cdf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 09:44:17 -0400 Subject: [PATCH 5/6] Add release input-coverage + reform-coverage smoke gate tests (#368) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The five acceptance cases from the brief, plus red-by-design and anti-drift guards, all isolated from policyengine-us (real Frame, a stub engine exposing only default_values, an injected simulate, and a monkeypatched reform builder): - full required set present with signal passes; - a missing required column fails, named; - a required column present but all-engine-default fails without an exclusion; - a reviewed exclusion whose column has caught up (present with signal) is stale and fails (#286 cannot-rot); - a bound reform scoring ~$0 fails the reform-coverage smoke (and a nonzero effect passes; a probe-less gate is refused). Shipped-manifest guards: the SSI assets stay required with no exclusion, the SSI probe binds through them, demoting an asset to a reviewed exclusion is rejected, and the committed manifest matches regeneration and names no retired data package — so neither the manifest nor the generator can silently rot. Co-Authored-By: Claude Fable 5 --- .../tests/test_release_input_coverage.py | 351 ++++++++++++++++++ 1 file changed, 351 insertions(+) create mode 100644 packages/populace-build/tests/test_release_input_coverage.py diff --git a/packages/populace-build/tests/test_release_input_coverage.py b/packages/populace-build/tests/test_release_input_coverage.py new file mode 100644 index 00000000..5c9d30e6 --- /dev/null +++ b/packages/populace-build/tests/test_release_input_coverage.py @@ -0,0 +1,351 @@ +"""US release input-column coverage + reform-coverage smoke, isolated from PE-US. + +populace #368. The five acceptance cases the brief pins: + +1. a full required set present with signal passes; +2. a missing required column fails, named; +3. a required column present but degenerate (every value the engine default) + fails without a reviewed exclusion; +4. a reviewed-exclusion column that has caught up (present with signal) is stale + and fails (#286 cannot-rot); +5. a bound reform scoring ~$0 fails the reform-coverage smoke. + +The frame is a real :class:`~populace.frame.Frame`; the engine is a stub exposing +only ``default_values`` (the surface the gate uses) and the simulation is injected +(and ``_build_reform`` monkeypatched), so nothing here imports policyengine-us — +the same isolation ``test_reform_validation`` relies on. Separate tests assert the +shipped manifest keeps the #368 red-by-design guarantee: the SSI countable-resource +assets stay hard requirements with no exclusion, and demoting one is rejected. +""" + +from __future__ import annotations + +import importlib.util +import json +from importlib.resources import files +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +import populace.build.us_runtime.reform_coverage_smoke as smoke_module +from populace.build.us_runtime import ( + SSI_COUNTABLE_RESOURCE_ASSETS, + US_RELEASE_INPUT_COVERAGE_RESOURCE, + ReformCoverageProbe, + ReleaseInputColumn, + ReleaseInputCoverageManifest, + assert_release_input_coverage_manifest_current, + load_release_input_coverage_manifest, + us_reform_coverage_smoke_gate, + us_release_input_coverage_gate, + us_release_reform_coverage_probes, +) +from populace.frame import EntitySchema, Frame, WeightKind, Weights + +_REPO_ROOT = Path(__file__).resolve().parents[3] +_MANIFEST_GENERATOR = ( + _REPO_ROOT / "tools" / "build_us_release_input_coverage_manifest.py" +) + + +def _person_frame(columns: dict[str, np.ndarray]) -> Frame: + """A real single-household Frame carrying ``columns`` on the person table.""" + n = len(next(iter(columns.values()))) + person = pd.DataFrame( + { + "person_id": np.arange(n, dtype="int64"), + "person_household_id": np.ones(n, dtype="int64"), + **{name: np.asarray(values) for name, values in columns.items()}, + } + ) + household = pd.DataFrame({"household_id": np.asarray([1], dtype="int64")}) + return Frame( + {"person": person, "household": household}, + EntitySchema(group_entities=("household",)), + {"household": Weights(values=np.asarray([1000.0]), kind=WeightKind.DESIGN)}, + ) + + +class _StubEngine: + """Only ``default_values(names)`` — the single engine surface the gate uses.""" + + def __init__(self, defaults: dict[str, object]) -> None: + self._defaults = dict(defaults) + + def default_values(self, names) -> dict[str, object]: + return {name: self._defaults[name] for name in names if name in self._defaults} + + +def _manifest( + columns: tuple[ReleaseInputColumn, ...], + probes: tuple[ReformCoverageProbe, ...] = (), +) -> ReleaseInputCoverageManifest: + return ReleaseInputCoverageManifest( + reference={"source": "test"}, columns=columns, probes=probes + ) + + +# A two-required-plus-one-excluded contract, reused across the gate cases. +_CONTRACT = _manifest( + ( + ReleaseInputColumn("employment_income", "required"), + ReleaseInputColumn("stock_assets", "required"), + ReleaseInputColumn( + "alimony_income", + "reviewed_exclusion", + reason="Residual income-source layer not yet sourced; tracked.", + issue="PolicyEngine/populace#38", + ), + ) +) + +# Every declared column defaults to 0.0 in the stub engine, so an all-zero +# required column reads as degenerate (present but indistinguishable from absent). +_DEFAULTS = {"employment_income": 0.0, "stock_assets": 0.0, "alimony_income": 0.0} + + +class TestReleaseInputCoverageGate: + def test_full_required_set_with_signal_passes(self) -> None: + # Case 1: both required columns present and carrying signal; the excluded + # column is absent (dormant), which is reported, not failed. + frame = _person_frame( + { + "employment_income": np.asarray([0.0, 52_000.0, 12_000.0]), + "stock_assets": np.asarray([0.0, 1_500.0, 0.0]), + } + ) + result = us_release_input_coverage_gate( + frame, _StubEngine(_DEFAULTS), manifest=_CONTRACT + ) + assert result.passed + assert result.failures == () + assert result.details["dormant_exclusions"] == ["alimony_income"] + + def test_missing_required_column_fails(self) -> None: + # Case 2: stock_assets is absent from the export entirely — the silent + # zero the #368 launch failure rode in on. + frame = _person_frame({"employment_income": np.asarray([0.0, 52_000.0])}) + result = us_release_input_coverage_gate( + frame, _StubEngine(_DEFAULTS), manifest=_CONTRACT + ) + assert not result.passed + assert "stock_assets" in result.details["missing"] + assert any( + "stock_assets" in failure and "absent" in failure + for failure in result.failures + ) + + def test_degenerate_required_column_without_exclusion_fails(self) -> None: + # Case 3: stock_assets is present but every value is the engine default, + # so the export writer's default-broadcast makes it indistinguishable + # from absence — and there is no reviewed exclusion to accept it. + frame = _person_frame( + { + "employment_income": np.asarray([0.0, 52_000.0]), + "stock_assets": np.asarray([0.0, 0.0]), + } + ) + result = us_release_input_coverage_gate( + frame, _StubEngine(_DEFAULTS), manifest=_CONTRACT + ) + assert not result.passed + assert "stock_assets" in result.details["degenerate_required"] + assert any( + "stock_assets" in failure and "default" in failure + for failure in result.failures + ) + + def test_stale_reviewed_exclusion_fails(self) -> None: + # Case 4: alimony_income is a reviewed exclusion, but the data caught up + # — it is now present with signal, so the exclusion is stale and must be + # promoted to a hard requirement (#286 cannot-rot). + frame = _person_frame( + { + "employment_income": np.asarray([0.0, 52_000.0]), + "stock_assets": np.asarray([0.0, 1_500.0]), + "alimony_income": np.asarray([0.0, 800.0]), + } + ) + result = us_release_input_coverage_gate( + frame, _StubEngine(_DEFAULTS), manifest=_CONTRACT + ) + assert not result.passed + assert result.details["stale_exclusions"] == ["alimony_income"] + assert any( + "Stale reviewed exclusions" in failure for failure in result.failures + ) + + def test_absent_and_degenerate_excluded_column_passes(self) -> None: + # The exclusion accepts both an absent column and a degenerate one: with + # alimony_income present-but-all-default, the reviewed exclusion holds. + frame = _person_frame( + { + "employment_income": np.asarray([0.0, 52_000.0]), + "stock_assets": np.asarray([0.0, 1_500.0]), + "alimony_income": np.asarray([0.0, 0.0]), + } + ) + result = us_release_input_coverage_gate( + frame, _StubEngine(_DEFAULTS), manifest=_CONTRACT + ) + assert result.passed + assert result.details["reviewed_exclusions"] == { + "alimony_income": "Residual income-source layer not yet sourced; tracked." + } + + +class _Series: + def __init__(self, total: float) -> None: + self._total = total + + def sum(self) -> float: + return self._total + + +class _Sim: + """A simulation whose weighted total for the measure is a fixed number.""" + + def __init__(self, total: float) -> None: + self._total = total + + def calculate(self, measure: str, period): # noqa: ARG002 - stub + return _Series(self._total) + + +def _probe(min_abs_effect: float = 1_000_000_000.0) -> ReformCoverageProbe: + return ReformCoverageProbe( + id="ssi_probe", + name="SSI asset limits raised to $10k / $20k", + parameter_changes={ + "gov.ssa.ssi.eligibility.resources.limit.individual": { + "2024-01-01.2100-12-31": 10000 + } + }, + budget_measure="ssi", + binding_inputs=("bank_account_assets", "stock_assets", "bond_assets"), + min_abs_effect=min_abs_effect, + reason="Assets absent → countable resources 0 → the relaxation scores $0.", + issue="PolicyEngine/populace#356", + ) + + +class TestReformCoverageSmokeGate: + def test_zero_bound_reform_fails(self, monkeypatch) -> None: + # Case 5: with the asset inputs absent, everyone already passes the SSI + # resource test, so raising the limit moves nothing — baseline and reform + # score the same total and the bound reform reads as a coverage hole. + monkeypatch.setattr(smoke_module, "_build_reform", lambda changes: "REFORM") + + def simulate(reform): # noqa: ARG001 - baseline == reform on purpose + return _Sim(4.0e10) + + result = us_reform_coverage_smoke_gate( + simulate=simulate, probes=[_probe()], period=2024 + ) + assert not result.passed + assert result.details["results"]["ssi_probe"]["effect"] == 0.0 + assert "did not bind" in result.failures[0] + assert "bank_account_assets" in result.failures[0] + + def test_bound_reform_with_effect_passes(self, monkeypatch) -> None: + # The green counterpart: when the assets are carried, the same reform + # moves SSI by ~$1.6B (the dense-native reference), clearing the floor. + monkeypatch.setattr(smoke_module, "_build_reform", lambda changes: "REFORM") + + def simulate(reform): + return _Sim(4.16e10 if reform == "REFORM" else 4.0e10) + + result = us_reform_coverage_smoke_gate( + simulate=simulate, probes=[_probe()], period=2024 + ) + assert result.passed + assert result.details["results"]["ssi_probe"]["effect"] == pytest.approx(1.6e9) + + def test_probeless_gate_is_refused(self) -> None: + # A probe-less smoke gate would pass vacuously — refuse it. + with pytest.raises(ValueError, match="at least one probe"): + us_reform_coverage_smoke_gate(simulate=lambda reform: _Sim(0.0), probes=[]) + + +class TestShippedManifest: + def test_manifest_is_current(self) -> None: + # The checked-in-facts half runs everywhere (no engine in this env): the + # declared surface must equal the reference eCPS populated columns and the + # SSI assets must stay hard requirements. + assert_release_input_coverage_manifest_current() + + def test_ssi_assets_are_required_without_exclusion(self) -> None: + manifest = load_release_input_coverage_manifest() + for asset in SSI_COUNTABLE_RESOURCE_ASSETS: + assert asset in manifest.required_columns + assert asset not in manifest.reviewed_exclusions + + def test_shipped_ssi_probe_binds_through_the_assets(self) -> None: + probes = us_release_reform_coverage_probes() + assert probes, "the shipped manifest must pin at least one reform probe" + ssi = next(probe for probe in probes if probe.id == "ssi_asset_limit_10k_20k") + assert set(SSI_COUNTABLE_RESOURCE_ASSETS) <= set(ssi.binding_inputs) + assert ssi.budget_measure == "ssi" + assert ssi.min_abs_effect > 0 + + def test_demoting_an_ssi_asset_to_exclusion_is_rejected(self) -> None: + # The #368 red-gate guarantee cannot be quietly undone: turning an SSI + # asset into a reviewed exclusion must fail the anti-rot assertion. + manifest = load_release_input_coverage_manifest() + tampered_columns = tuple( + ReleaseInputColumn( + column.name, + "reviewed_exclusion", + reason="pretend this gap is tracked", + issue="PolicyEngine/populace#000", + ) + if column.name == "stock_assets" + else column + for column in manifest.columns + ) + tampered = ReleaseInputCoverageManifest( + reference=manifest.reference, + columns=tampered_columns, + probes=manifest.probes, + ) + with pytest.raises(ValueError, match="stock_assets"): + assert_release_input_coverage_manifest_current( + manifest=tampered, engine=None + ) + + +def _load_manifest_generator(): + spec = importlib.util.spec_from_file_location( + "build_us_release_input_coverage_manifest", _MANIFEST_GENERATOR + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +class TestManifestGeneratorSync: + def test_committed_manifest_matches_regeneration(self) -> None: + # The committed manifest is derivable purely from the checked-in eCPS + # parity reference + known-gaps register, so it cannot drift from them: + # regenerating must reproduce the committed file byte-for-value. + generator = _load_manifest_generator() + committed = json.loads( + files("populace.build.us") + .joinpath(US_RELEASE_INPUT_COVERAGE_RESOURCE) + .read_text(encoding="utf-8") + ) + assert generator.build_manifest() == committed + + def test_generated_manifest_names_no_retired_data_package(self) -> None: + # The manifest is not on the incumbent-reference allow-list, so its + # provenance block must not name the retired data package (the guard + # test_us_plan.test_no_incumbent_data_package_references_in_live_tree + # enforces on the committed file; this pins the generator too). + generator = _load_manifest_generator() + rendered = json.dumps(generator.build_manifest()) + # Build the needles by concatenation so this test file does not itself + # trip the live-tree guard it mirrors (test_us_plan does the same). + assert ("policyengine-" + "us-data") not in rendered + assert ("policyengine_" + "us_data") not in rendered From 33e1a9009c11b1eb3a82ed36553229a4908480a8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 9 Jul 2026 10:49:31 -0400 Subject: [PATCH 6/6] Merge origin/main; regenerate coverage manifest against advanced gaps register (65 required / 93 exclusions / 1 probe) (#368) Co-Authored-By: Claude Fable 5 --- .../us/release_input_coverage_manifest.json | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json index 7184dd81..a1052743 100644 --- a/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json +++ b/packages/populace-build/src/populace/build/us/release_input_coverage_manifest.json @@ -220,9 +220,7 @@ "status": "reviewed_exclusion" }, "hours_worked_last_week": { - "issue": "PolicyEngine/populace#242", - "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", - "status": "reviewed_exclusion" + "status": "required" }, "household_vehicles_owned": { "issue": "PolicyEngine/populace#49", @@ -240,9 +238,7 @@ "status": "reviewed_exclusion" }, "immigration_status_str": { - "issue": "PolicyEngine/populace#38", - "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", - "status": "reviewed_exclusion" + "status": "required" }, "investment_income_elected_form_4952": { "issue": "PolicyEngine/populace#274", @@ -481,9 +477,7 @@ "status": "reviewed_exclusion" }, "ssn_card_type": { - "issue": "PolicyEngine/populace#38", - "reason": "Demographic / occupation / labor-status descriptor input carried by the incumbent but not yet on the candidate frame; remaining input-layer work (#38).", - "status": "reviewed_exclusion" + "status": "required" }, "sstb_self_employment_income_before_lsr": { "issue": "PolicyEngine/populace#298", @@ -534,9 +528,7 @@ "status": "reviewed_exclusion" }, "takes_up_eitc": { - "issue": "PolicyEngine/populace#312", - "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", - "status": "reviewed_exclusion" + "status": "required" }, "takes_up_head_start_if_eligible": { "issue": "PolicyEngine/populace#312", @@ -549,9 +541,7 @@ "status": "reviewed_exclusion" }, "takes_up_medicaid_if_eligible": { - "issue": "PolicyEngine/populace#312", - "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", - "status": "reviewed_exclusion" + "status": "required" }, "takes_up_medicare_if_eligible": { "issue": "PolicyEngine/populace#312", @@ -567,9 +557,7 @@ "status": "reviewed_exclusion" }, "takes_up_tanf_if_eligible": { - "issue": "PolicyEngine/populace#312", - "reason": "Stochastic take-up flag the incumbent seeds so take-up-gated programs do not ship at 100% participation; Populace does not yet produce it.", - "status": "reviewed_exclusion" + "status": "required" }, "tax_exempt_interest_income": { "status": "required" @@ -653,9 +641,7 @@ "status": "reviewed_exclusion" }, "weekly_hours_worked_before_lsr": { - "issue": "PolicyEngine/populace#242", - "reason": "Hours-worked / hourly-wage / labor input not yet carried through Populace US outputs (#242).", - "status": "reviewed_exclusion" + "status": "required" }, "weeks_unemployed": { "issue": "PolicyEngine/populace#38", @@ -679,8 +665,8 @@ } }, "counts": { - "required": 58, - "reviewed_exclusion": 100, + "required": 65, + "reviewed_exclusion": 93, "total": 158 }, "derivation": "Required surface = ecps_parity_reference.json populated layers (input columns the pinned, sha-verified reference eCPS populates). status='reviewed_exclusion' for ecps_parity_known_gaps.json entries (reason+issue from that register); EXCEPT the SSI countable-resource asset inputs (bank_account_assets, stock_assets, bond_assets), which are status='required' with NO exclusion per PolicyEngine/populace#368 so the gate fails on today's artifacts and asset restoration (Deliverable 2) turns it green. All other populated layers are 'required'. Regenerate with tools/build_us_release_input_coverage_manifest.py.",