Skip to content

Restore ASEC ED_VAL via a pinned sidecar and close the source-carry class - #421

Merged
MaxGhenis merged 1 commit into
mainfrom
asec-education-source
Jul 13, 2026
Merged

Restore ASEC ED_VAL via a pinned sidecar and close the source-carry class#421
MaxGhenis merged 1 commit into
mainfrom
asec-education-source

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

The incident chain

base-r3 (the first full-scale base past the QRF wall and the child-support gate) failed at stage 17: education_inputs requires raw ASEC ED_VAL, and no census_cps h5 ever carried it — the archived pipeline read it straight off the survey (cps.py:1493: educational_assistance = person.ED_VAL, a direct carry with no recode) while the frozen h5 generation dropped the column. A 23-constant sweep found exactly one such gap (the three org_wages columns are release-time and satisfied upstream there).

The fix — the weeks_unemployed pattern, extended

LKWEEKS had the identical problem for one cohort and solved it with a pinned official-archive sidecar; this PR extends that pattern to every pooled income year:

  • Pinned archives per income year (zip + member sha256/size/CRC + row/audit pins): survey 2023 (d2e00025…), 2024 (cdb39cda…), 2025 (318845a2…).
  • Income year ↔ survey year+1, verified empirically: each income-year cohort has 100.0% PERIDNUM coverage in its survey-year+1 archive and only ~33% (rotation-group) in adjacent vintages — so a wrong pin fails the exact join loudly instead of silently degrading. The survey-2023 pins are byte-identical to the ones weeks_unemployed already reviewed, cross-validating the download path.
  • Hermetic: --asec-education-source INCOME_YEAR=PATH consumes local verified copies; omitted years fetch from census.gov and verify against the same pins. The fill is an exact per-year PERIDNUM join with redundant PH_SEQ/P_SEQ/A_LINENO identity checks, refuses uncovered keys/years, and never overwrites or predicts a value.

The second find — a mis-set band, adjudicated with evidence

Replaying the exact failed call (base-r3 checkpoint 016 + the real sidecar): the stage completes, educational_assistance lands at 2.09% weighted on both channels (survey-measured, channel-symmetric — as it should be). That replay exposed the stage's other band: qualified tuition showed 0.288% against a [0.003, 0.05] floor. Decomposition proves this is the designed value, not a deficiency: tuition is a PUF-only tax-detail chain target, exactly zero on the ASEC channel like every other PUF-only detail column (partnership income, charitable donations, mortgage interest all decompose identically on the full-scale pool), with the PUF half at the PUF-faithful 0.576%. The original floor encoded the retired file's blended 2.64% — a post-imputation file rate that isn't comparable to the pre-selection two-channel pool. The gate now asserts the channel invariant directly: ASEC exactly zero (a contamination tripwire), PUF share in [0.002, 0.05], total in [0.001, 0.05]. Verified passing on the real frame.

The closure test

TestBaseStageSourceClosure makes this failure class static: every base-stage REQUIRED_SOURCE_COLUMNS entry must be covered by the pinned census_cps person columns (162, frozen in the test), pool-constructed identity columns, a pinned sidecar restoration, or another stage's declared outputs. Adding a stage requirement with no provider now fails CI in seconds instead of a full-scale build in 35 minutes.

Verification

  • Replay of the exact failed stage call on base-r3 ck016: completes, gate passes, channel report above.
  • 33 tests across the five affected files (10 new sidecar tests covering every fail-closed path with synthetic pinned archives; the closure test; incumbent education/builder/coverage suites) — all green.
  • ruff (CI version) check + format clean.

🤖 Generated with Claude Code

…lass

The frozen census_cps h5s never carried raw ASEC ED_VAL (the archived
pipeline read it directly: cps.py:1493 educational_assistance =
person.ED_VAL, a direct carry), so the education-inputs stage failed the
first full-scale base 17 stages in. Restore it with the weeks_unemployed
sidecar pattern extended to every pooled income year: per-year pinned
official Census archives (zip+member sha256/size/CRC and audit pins),
fetch-or-local via --asec-education-source INCOME_YEAR=PATH, and an
exact per-income-year PERIDNUM join with redundant identity checks that
never predicts a value. Income years map to survey-year+1 archives —
verified by 100.0% PERIDNUM coverage per year (adjacent vintages only
overlap the ~33% rotation group, so a wrong pin fails the join loudly).

Replaying the exact failed call (base-r3 checkpoint 016) with the
sidecar: the stage completes and educational assistance lands at 2.09%
weighted on both channels. That replay exposed the stage's other band as
mis-set: qualified tuition is a PUF-only tax-detail chain target, so the
ASEC channel is exactly zero BY DESIGN (partnership income, charitable
donations, and mortgage interest all decompose the same way) and the
pool-wide share is 0.288%, not the retired file's blended 2.64%. The
gate now asserts the channel invariant directly — ASEC exactly zero
(contamination check), PUF share in [0.002, 0.05], total in
[0.001, 0.05] — and passes on the real frame.

A new closure test makes this failure class static: every base-stage
REQUIRED_SOURCE_COLUMNS entry must be covered by the pinned census_cps
person columns, pool-constructed identity columns, a pinned sidecar
restoration, or another stage's declared outputs. The sweep that
motivated it found exactly one gap (ED_VAL); the three org_wages
columns are release-time and excluded with documentation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis merged commit d930634 into main Jul 13, 2026
4 checks passed
MaxGhenis added a commit that referenced this pull request Jul 13, 2026
#421 added the education sidecar to the locked run config but not to
_stage_cli_args, so --stage all locked the parent context with the
sidecar and spawned every child without it: the first child recomputed
a config lacking asec_education_source, hit the run-context guard, and
base-r4 died in six seconds. Forward the flag beside the other appended
inputs, and pin the whole class with a round-trip regression test:
_stage_run_config(parent) must equal _stage_run_config over the
_stage_cli_args-reconstructed child argv, so any future input that
enters the run config without entering the reconstruction fails in CI.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant