Skip to content

Gate validation-config input coverage against source-stage outputs#311

Merged
MaxGhenis merged 4 commits into
mainfrom
validation-input-gate
Jul 5, 2026
Merged

Gate validation-config input coverage against source-stage outputs#311
MaxGhenis merged 4 commits into
mainfrom
validation-input-gate

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Fixes #272.

Why

The reform-validation configs (obbba_reforms.json, tax_expenditure_reforms.json, soi_baseline_levels.json) score policy provisions on the calibrated dataset. A provision's simulated effect is driven by pure-input leaves — PolicyEngine-US variables with no formula, whose values must come from a source stage. When a provision's critical leaf is produced by no stage, the provision is a structural zero and the row validates as a silent zero until an external benchmark exposes it. Nothing checked this, which is exactly how the recent gaps stayed invisible:

What

A release gate that fails the build when a validation row's provision-critical input leaf is neither produced by a source stage nor allowlisted with a reason.

  • source_stage_input_coverage_gate (pure, in gates.py): given {leaf -> validation rows that need it}, the declared source-stage outputs, and reviewed exclusions, it passes iff every required leaf is a declared output or carries a reason. Mirrors the reviewed-exclusion, stale/dormant, and named-failure conventions of the surrounding gates (a failure names the leaf and every affected row).
  • us_validation_input_coverage_gate + the US_VALIDATION_PROVISION_INPUT_LEAVES registry (in us_runtime/validation_input_coverage.py): resolves the declared surface from source_stages.json (typed SourceManifest read) and runs the pure gate. Seeded with the two invisible-gap instances above, each allowlisted with its open issue.
  • assert_validation_leaf_registry_current — anti-rot: each registry entry is checked against the live PolicyEngine-US graph via the engine tracer (the leaf must be a pure input and an input-leaf dependency of its provision), the reverse-direction check that keeps the other engine-derived guards from rotting. A no-op without the [us] extra.
  • Wired into build_us_fiscal_refresh_release.py as an early preflight in main (before the expensive calibration, next to the CD-vintage preflight), and recorded in calibration_diagnostics.json under release_gates.validation_input_coverage.

Scope decision — curated critical leaves, not the transitive closure

The issue's literal phrasing is "every pure-input leaf a config variable depends on." Applied as the full transitive closure this is intractable as a gate: education_tax_credits/cdcc each touch 195 input leaves, and even after unioning every packaged input surface (source-stage outputs + CPS-carried + PUF defaults + structural) the residual undeclared set is 145 leaves — energy-credit expenditures, clean-vehicle flags, state-specific earnings, take-up flags. A gate failing on 145 leaves behind a 145-entry allowlist would drown the real signal and would never have singled out #252/#253. The registry instead names, per validation concern, the leaf whose absence zeroes that provision — the small high-signal set the issue's evidence table describes — and is kept honest by the live-graph consistency check rather than being a static list that silently rots. A new validation row whose provision keys on an un-imputed, un-allowlisted leaf fails CI here instead of shipping a silent zero.

Open question for review: this catches gaps for registered provisions. If you'd rather catch every new un-imputed leaf automatically (at the cost of a ~150-entry reasoned allowlist and churn as PolicyEngine-US adds variables), the pure gate already supports that mode — say the word and I'll widen the registry to the full closure.

Tests

TDD, failing-first. The plant-a-missing-leaf tests prove both the pure gate and the public entry point fail loudly (with the variable and affected row named) when a validation row keys on an un-imputed, un-allowlisted input leaf; passing cases and stale/dormant-exclusion handling are covered too. The live-graph registry-consistency test runs where the [us] extra is installed.

Local, reproducing each CI job:

  • test job (uv sync --all-packages, no [us] extra): uv run pytest1348 passed, 48 skipped; uv run ruff check . — clean.
  • wheels job (built wheels installed into a clean venv, no [us] extra): pytest packages/*/tests1309 passed, 57 skipped; the new module is present in the built populace_build wheel and importable.
  • With the [us] extra installed, the live-graph registry check passes against real PolicyEngine-US.

Note on #307

This branch originally also carried a deflake for test_refit_l0_selection_threads_l2_lambda_to_refit (#307). PR #309 (l2-anchor-semantics) already fixes #307 at the root cause — it replaces the noisy ESS-direction assertion with the threading contract and shows the direction this branch's epsilon asserted actually inverts at production scale — so that change was dropped and this PR is #272-only, with no overlap on test_solve.py.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits July 4, 2026 19:25
A validation config scores a policy provision whose effect is driven by
pure-input leaves (engine variables with no formula). If a row's critical leaf
is produced by no source stage, the provision is a structural zero and the row
validates as a silent zero until an external benchmark exposes it
(#252 auto-loan qualifying interest, #253 education
tuition). This pure gate fails the build when a required leaf is neither a
declared stage output nor allowlisted with a reason.

Keyed on curated provision-critical leaves, not the full transitive input
closure of the row measure: the closure of a broad measure like income_tax is
the whole tax base (hundreds of legitimately-defaulted leaves), so a closure
gate would drown the signal. Mirrors the reviewed-exclusion, stale/dormant, and
named-failure conventions of the surrounding gates.

Refs #272

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
us_validation_input_coverage_gate builds the named US gate: every
provision-critical input leaf the reform-validation configs depend on must be a
declared source_stages.json output or carry a reviewed exclusion with its
tracking issue. Seeded with the two invisible-gap instances (#253
qualified_tuition_expenses, #252 qualified_passenger_vehicle_loan_interest),
each allowlisted with its open issue.

The registry names the provision-critical leaves per validation row, not the
transitive input closure of the row measure. assert_validation_leaf_registry_
current proves each entry against the live PolicyEngine-US graph via the engine
tracer — the leaf must be a pure input and a dependency of its provision — so
the registry cannot rot as the engine evolves. Both engine-dependent paths
degrade to a no-op without the [us] extra, so the base test suite and the wheels
job (no policyengine-us) pass unchanged.

The failing-first plant-a-missing-leaf tests prove both the pure gate and the
public entry point fail loudly when a validation row keys on an un-imputed,
un-allowlisted input leaf.

Refs #272

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runs us_validation_input_coverage_gate() as an early preflight in main (before
the expensive calibration, next to the CD-vintage preflight), after checking the
registry against the live PolicyEngine-US graph. A validation row keying on an
un-imputed, un-allowlisted input leaf now fails the build with a named
release-gate error instead of shipping a silent structural zero (#252/#253). The
gate result is also recorded in calibration_diagnostics.json under
release_gates.validation_input_coverage for auditability.

Refs #272

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…input-gate

test_gates.py: keep main's TestFitWeightRecord/TestWeightsAuditGate and this
branch's TestSourceStageInputCoverageGate; both are pure additions whose
conflict came from coincidentally identical closing lines.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis MaxGhenis force-pushed the validation-input-gate branch from d5e390d to 331ee56 Compare July 5, 2026 02:47
@MaxGhenis MaxGhenis merged commit 7fdde8b into main Jul 5, 2026
4 checks passed
@MaxGhenis MaxGhenis deleted the validation-input-gate branch July 5, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant