Refuse US releases that store model inputs the certified engine does not define (#1026) - #1031
Merged
Merged
Conversation
…1026) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ster digest Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y the written H5's verdict Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ories Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and writer model Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ontract Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt-guide note Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ut check test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e staging metadata series
Registers {entity}_spine_source_id, {entity}_spine, the six ACS-native acs_*
amounts and puma_geoid, each bound to its producer by test, and adds the
receipts child, the Build Q stacked pool and the 9/23 ACS local-area release
to the examined-file inventories. The metadata reader accepts
_populace_staging_metadata and _time_period only as pandas series, and a
Hypothesis property now covers the reader's refusal half.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The package stage records the installed policyengine-us as built-with, so it now refuses a calibrated H5 that stores a model input that engine does not define, before any release directory exists, and records the verdict as the stored_inputs gate bound to the packaged bytes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the three seams in the docs When the stored-input contract refuses inside the native-loader probe, the compatibility check now also reports a built-with engine that differs from the installed one, which a passing probe would have reported. The agent guide, README and changelog name the three guarded seams instead of claiming every US release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… structure Two harness modes drive _main to each stored-input abort: a refusal is the batched pre-export raise's only line and no H5 is written; a premise failure aborts after the write and before the post-export scorer opens. The wiring test now locates each statement in _main's own body, so a join that is commented out, made conditional, or an abort that no longer raises fails it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…put module docstring Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ite direction; draw monotonicity from the stored pool; correct the Part B classification and the reader and core docstrings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…changelog, README and runbooks policyengine-us 1.452.0 through at least 1.670.2 define it as a Person, YEAR, float input; 1.690.7 had replaced it with medicare_part_b_premiums_reported. The old wording said no engine version defines it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ry engine version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ous range The changelog, the register comment and two test comments now say the old name is an input in every policyengine-us version read from 1.452.0 to 1.670.2 (the nine in the local uv cache) and that every version read from 1.690.7 to 2.15.1 (91) defines only medicare_part_b_premiums_reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-3 review P2/P3s on #1031, each verified against source: - In policyengine-us 1.670.2, health_insurance_premiums adds medicare_part_b_premiums, but spm_unit_medical_out_of_pocket_expenses subtracts it and adds income_adjusted_part_b_premium in its place (it did not add it). - policyengine-us 1.777.0 renamed would_claim_wic, not 2.x: the cached versions read bracket it (1.775.8 old name only, 1.779.4 new name only), and release_input_coverage.py records 1.777.0. - The Route A rehearsal export was written by the Route A integration tool (8f63bf0 on route-a-integration-20260923, per its rebuild receipt), not by main's tools. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Final-delta review P3s on #1031: - The Route A rehearsal export was rebuilt by the R4 rehearsal script from run 8f63bf0's vaulted checkpoint and written with the fiscal-refresh tool's writer at 8f63bf0, not by a "Route A integration tool". - The builder moved to takes_up_wic_if_eligible in 05d254a (the policyengine-us 1.819.0 bump); #746 was closed as not planned. - The 1.777.0 rename is cited to the policyengine-us changelog entry (2026-07-21) as well as the cached-version bracket. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Sep 26, 2026
Brings in the 19 main merges since 6442030: the Route A remediation stack (#1016, #1018, #1017, #1025, #1024, #1028), #1029, #1005, #1008, #1004, #992, #1031, #1015, #1033, #994, #954, #966, #1006 and #1010. Eight conflicted paths, each recorded in docs/native-integration-20260923.md: - test.yml keeps the native sharded matrix (main's --durations=25 is already in every native pytest call); - the us_runtime facade stays lazy and gains main's three fiscal-target exclusion exports (the parent union, 920 names; the facade-union test is re-pinned to that union's digest); - reform_validation.py keeps main's _released_engine_state and the native explicit-constructor default_simulate_factory; - build_us_fiscal_refresh_release.py carries the native explicit consumer seams (formula metadata, dataset and microsimulation constructors, SPM selection) onto main's household-batched post-export scorer and batched base materialization; omitted seams keep main's exact calls, and _main supplies none. Three native test files that addressed the removed frame-based factory now address the scorer with the same assertions; - identity pins observed on the merged tree: five EXPECTED_HASHES entries, the regenerated F0 coverage report (42,239/42,239 fields, 41/41 checks), the US spec identity 2dfa51b8... and the loader golden f2047cb9... tools/generate_us_bundle_from_constants.py --check, tools/ci_test_groups.py --verify, the CI matrix contract and ruff pass. The fiscal consumer, formula-metadata, shared target/solve and calibration-attachment files pass; the rest of the fiscal battery and the known pre-existing failures are follow-up work on this branch. No actual-data native run, release certification or publication. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the recurrence half of #1026 (decision d271). It does not re-cut any published release.
What this adds
policyengine-core loads a stored column as an input only when the engine defines a variable of that name. Before that loop it reads the structural columns that build the entities:
person_id, each{group}_idandperson_{group}_id(all variables of policyengine-us 2.2.1) and, when stored, the role columnperson_{group}_roleorrole(not variables). Every column that is not a variable is named in one logged warning and otherwise ignored (Simulation.build_from_dataset, read in policyengine-core 3.32.5).populace-us-2024-spm-20260915and its reported-receipt child were both certified against policyengine-us 2.2.1. Both still store the WIC take-up draw aswould_claim_wic, a name 2.2.1 does not define, so the draw was ignored.microcosm.data.stored_inputsrefuses a US release if a stored table has a column that meets all three conditions:[a-z][a-z0-9_]*, the engine's naming convention (measured below);US_STORED_NON_VARIABLE_COLUMNS({column: reason}, 49 entries).Each refusal is one line. The line quotes the column, names every table that stores it and the engine version, and gives both remedies: rename it to its live input (or stop storing it), or add a reviewed register entry with its reason.
Where it runs, and why there
The check needs the variable list of the engine the release is certified against. It runs at the three seams where that engine is provably the one loaded in the process, because the same process records it as
build.built_with_model_package:tools/build_us_fiscal_refresh_release.py: a batched pre-export gate. It grades the export frame's stored tables. A refusal joins the single batched pre-export raise, so every other failing gate is reported with it and no H5 is written. The exact-k ladder lane (tools/build_us_exact_k_ladder_release.py) runs this tool, so it passes the same gate. The gate works from a model of what the writer stores (_export_stored_tables): the frame's non-empty tables plus the materializedhousehold_weight. Right after the write,_written_stored_input_verdict_mismatchgrades the written file's HDF metadata, whatever the gate reached. If the gate evaluated, the written file must refuse exactly the columns the gate refused. If the gate could not evaluate (reachable only when evidence mode owns that failure line), the written file must refuse nothing, because no refusal of any column was reviewed. A written file it cannot grade (unlistable tables, or no engine) is reported too. Any of these aborts the run; this is a premise failure that evidence mode cannot convert. Evidence mode (--evidence-release) can record a stored-input refusal as an owned known failure, as it can for every batched gate.run_native_loader_compatibility, the source-enrichment probe. The check runs before either loader. The probe runs in the tested runtime at--certify, and again whenever the contract replays it: validation, publisher--preflight-only, and publication._check_compatibilityalready refuses a bundle whosebuilt_with_model_packageis not the runtime the probe tested. When the check refuses inside the probe,_check_compatibilitynow also reports that built-with mismatch, first, so a refusal graded against an engine the bundle was never certified with is not read as a verdict on it. Both affected releases went through this lane. The receipt gains astored_inputsblock, holding the register's sha256 and the registered non-variable columns the H5 stores, plus acountry:defines_every_stored_model_inputcheck label.tools/build_us_acs_local_release.py, the ACS local-area package stage. It grades the calibrated H5's metadata before the release directory exists, and records the verdict as thestored_inputsgate, bound to the packaged bytes, ingate_summary.jsonandbuild_manifest.json. A refusal or an engine that cannot be imported stops packaging with nothing written.Two other places were considered and not used:
contract.py'svalidate_release_dirchecks calibration releases from JSON manifests and hashes only. It has no engine, and nothing ties the publishing environment to the built-with version.tools/preflight_us_release_gates.pyis advisory. It grades the base pool before the solve, not the export that ships.Annual static-aging projections (
microcosm.build.us_annual_static_aging) are built from a certified base release and are not re-checked here.python -m microcosm.data.stored_inputs <h5>...prints a JSON verdict per file from HDF metadata alone. The five files below took 9.4 s together in one process, with a 1.0 GB peak RSS; most of that is the engine import.Naming convention, measured (policyengine-us 2.2.1)
[a-z][a-z0-9_]*.in_state.py._or a digit.requires_ustest confirms it refusesCA), and the ACS local-area lane holds every formula-owned column back (project_input_only, classified byPolicyEngineUSEngine.formula_owned_outputs).A_AGE,H_TENURE, ...) pass by rule. The examined files store 163 to 189 uppercase columns each, 191 distinct; none is an engine variable and none is a state code. The only two-letter ones are the ACS PUMS fieldsSTandNP.The H5 reader
h5_stored_tablesreads each top-level pandas frame's column labels (table and fixed format). Two top-level keys are metadata, not tables:_time_periodand the_populace_staging_metadataseries that Microcosm's nullable writer (us_runtime.h5_io) adds, which the ACS local-area release and the multispine pools carry.USSingleYearDatasetreads only the six entity tables and_time_period(read in policyengine-us 2.2.1). Each metadata key must be a pandas series, which has no columns. Anything else is refused rather than guessed: avalues_block_*field, a missing or displaced index field, a non-frame object, a series under an unknown key, or a metadata key stored as a frame.Only top-level objects are read. A frame pandas nests under an entity frame's group (
person/extra) is not listed, and a test pins that. It suffices because policyengine-us 2.2.1 loads an H5 path throughUSSingleYearDataset, which reads only the six top-level entity frames and_time_period.USMultiYearDatasetreads year-keyed frames (person/2024), but only when a caller builds it from a file explicitly; that layout leaves the top-level entity groups untyped, and a test pins that the reader refuses it.Core's role columns.
person_{group}_roleandroleare model-named, not variables, and read by core. The rule does not exempt them, so a file storing one would be refused by name: a fail-closed false refusal, never a silent pass. No examined file stores one; the only examined column ending in_roleis the engine variableis_spm_independent_minor_role.test_the_check_refuses_core_role_columns_and_no_examined_file_stores_onepins both, and arequires_ustest pins that none of the six names is a 2.2.1 variable.The register and its evidence
Every entry is a model-named, non-variable column stored by at least one examined file.
test_every_register_entry_is_stored_by_a_file_examined_for_1026enforces this againsttests/fixtures/stored_input_inventories.json, the stored column names of the five files below.test_only_the_acs_lane_spine_tags_rest_on_refused_files_alonerequires every entry to be stored by one of the two files that pass (the rehearsal export and the stacked pool), except the six*_spinetags. Those are stored only by the ACS local-area release, which is itself refused for the two retired inputs; they stay registered on the strength of their live producer,base_pool.spine_column, whichtest_us_stored_input_register.pybinds.test_every_register_entry_is_a_named_producer_columnrequires the register to equal exactly the union of the producer definitions below.{entity}_source_id,{entity}_support_channel,{entity}_support_clone_index,{entity}_spine_source_idfor the 6 US entities (24)us_runtime.support_provenance.support_*_columnandspine_source_id_columnoperator_boundary'ssupport_provenancefamily declares. The source ids can key seeded draws (spec_engine.seeds,us_runtime.take_up). The spine source id is the raw id before assembly remaps collisions;puf_supportrequires it on a preassembled frame and the stacked-spine lineage receipts bind it.{entity}_spinefor the 6 US entities (6)us_runtime.base_pool.spine_columnasec_puforacs_2024_1yr) written whenacs_multispinepools the ACS spine for the ACS local-area lane;spm_universe_sourceandacs_local_hoursread it.source_year,source_household_id,source_person_id,source_row_id(4)outer_stage_runtime._POOLED_SOURCE_PROVENANCE_COLUMNS, assigned byasec_poolandacs_pumssource_row_idlinks a record back to its source row.tax_unit_role_input,filing_status_input(2)microcosm.frame.units(microunit output)filing_statusandis_tax_unit_head/_spouse/_dependentby formula, and the writer refuses to store those.puf_capital_gains_tail_*(6)us_runtime.puf_capital_gains_tail*_COLUMNconstantsassert_puf_capital_gains_tail_survives_selection.acs_social_security_income,acs_retirement_income,acs_interest_dividend_rental_income(3)operator_boundary._ACS_NATIVE_INPUT_CONTRACTS(theacs_keys), mapped byacs_inputsacs_transfer._RECIPIENT_COMBINED_SOURCES). A test binds each reason's input list to_DONOR_COMBINED_COMPONENTS, and arequires_ustest checks each is an engine input.acs_monthly_contract_rent,acs_monthly_gross_rent,acs_annual_property_tax(3)pre_subsidy_rentby the ACS transfer andreal_estate_taxesfrom TAXAMT on the reference person.puma_geoid(1)us_runtime.acs_pums(source-bound by test)pumainput with the same value; the engine readspuma, a household input in 2.2.1.Two columns are deliberately not registered. Each is a retired engine input, not metadata:
would_claim_wic. policyengine-us 1.777.0 renamed it (its changelog entry for 1.777.0, 2026-07-21; the cached versions read agree: 1.775.8 defines only the old name, 1.779.4 only the new); 1.764.6 defines it and 2.2.1 does not. The live input istakes_up_wic_if_eligible, and the builder moved to it in 05d254a (the policyengine-us 1.819.0 bump).medicare_part_b_premiums. The ASECPEMCPREMtransfer target, stored under a retired input name: the same defect class aswould_claim_wic. Every policyengine-us version read from 1.452.0 to 1.670.2 (nine) defines it as a Person, YEAR, float input with no formula. In 1.670.2health_insurance_premiumsadds it, andspm_unit_medical_out_of_pocket_expensessubtracts it and adds the rules-basedincome_adjusted_part_b_premiumin its place. By 1.690.7 it had been replaced bymedicare_part_b_premiums_reported, the same Person, YEAR, float input under a new name. Every version read from 1.690.7 to 2.15.1 (91, 1.764.6 and 2.2.1 included) defines only the new name, and nothing in 1.690.7, 1.764.6, 2.2.1 or 2.15.1 reads it; the engine computesmedicare_part_b_premiumitself. Transfer targets must be engine input leaves: drop formula-owned weeks_worked, fix medicare premiums leaf, add ownership guard (#578 inc 2) #590 (f53032f) dropped the transfer from the build, and the rehearsal export no longer stores it, so the remedy is to stop storing it. Registering it would allow a model-named column that the engine ignores.Evidence for these two, and for the two tax-unit columns, comes from reading source. For policyengine-us 1.764.6 that is the cached wheel, searched with grep and not imported; for 2.2.1 it is the installed package and its live
system.variables. For the Part B history: thevariablestrees of all 100 policyengine-us versions unpacked in the local uv cache (1.452.0 to 2.15.1) were grepped for both class definitions. The 9 from 1.452.0 to 1.670.2 define onlyclass medicare_part_b_premiums(Variable)(inhousehold/expense/health/medicare_part_b_premiums.py), each asvalue_type = float,entity = Person,definition_period = YEARwith no formula,addsorsubtracts; the 91 from 1.690.7 to 2.15.1 define onlyclass medicare_part_b_premiums_reported(Variable), each with the same three attributes and no formula,addsorsubtracts. Archives holding fewer than 500 variable files (partial unpacks) were excluded. The whole package of 1.452.0, 1.670.2, 1.690.7, 1.764.6, 2.2.1 and 2.15.1 was grepped for both names, which is how the consumers above were found. The cache holds no version between 1.670.2 and 1.690.7, so when inside that gap the rename landed is not known here, and no version before 1.452.0 was read.Results on the examined files
populace-us-2024-spm-20260915(same bytes as-20260909), read from the local HF cache6496cc43…aee84medicare_part_b_premiums,would_claim_wic(person table). 377 stored columns; 24 registered non-variables.populace-us-2024-spm-receipts-20260923, the local certification candidate00946972…89221c, equal to the published manifest's artifact hashpopulace-us-2024-buildo-acs-local-767312d60-20260923T074941Z, the local package artifact (built with 2.2.1, donor: the receipt child)769756c3…b6a7ec, equal to the published manifest's artifact hashroute-a-integration-20260923, per its rebuild receipt)58304078…07f7c3fus_input_pool_f010_s578.h5(pipelineus-stacked-pool, built 2026-09-16), the kind of pool--base-h5takes7b626086…e55754, equal to its manifest*_spine_source_id, the sixacs_*andpuma_geoid.populace-us-2024-buildp-sparse-rmloss100-cae8640-20260728T011454Z(extra check)48b9d479…94c7eThe fixture's column listings match pandas'
HDFStorestorer metadata exactly, in names and order, on every file.Consequence for the live lanes. Every pinned parent stores the two refused columns: BuildP for the role lane, the national default for the receipt lane, and the receipt child for the ACS local-area lane. So none of the three can certify or package a new release under 2.2.1 until its parent or donor is re-cut with the live WIC name and without the retired Part B column. Replaying the contract on the published default or the receipt bundle now refuses them, which is what d271 asked for. The runbooks say so. The stacked-pool lanes (fiscal refresh with a pool
--base-h5, exact-k ladder) pass on the Build Q pool's columns.Receipt shape. The probe's receipt gains a
stored_inputsblock and one check label, and_check_compatibilityrequires the stored receipt to equal the replayed one. So a source-enrichment receipt certified before this change no longer matches a replay: republishing such a bundle needs re-certification. Both existing source-enrichment bundles are refused by the check itself anyway. Consumers are unaffected: only the publisher paths (prepare_release,validate_release_dir) replay the probe.Invariants, and how each is tested
Property tests use Hypothesis. They import it inside each test, as the repo already does, because the wheels job has no Hypothesis.
test_property_refusal_if_and_only_if_and_the_error_names_exactly_those, 300 examples, checked against an independently written model-name oracle, plus example tests.test_property_adding_a_variable_or_register_entry_never_refuses_more, which draws the engine and the register from the stored columns (as the refusal property does), so they overlap them. It usually kills a rule whose verdict on a registered column depends on the register's size (a reviewer measured 29 of 30 fresh Hypothesis databases); the refusal property always does.test_property_register_consistency_flags_exactly_the_dead_or_bare_entries. The shipped register is checked against the installed engine (requires_us) and engine-free.test_property_the_verdict_ignores_table_and_column_order_and_repeats.test_property_h5_metadata_round_trips_the_stored_columnsover random table and fixed layouts with either metadata series, plus a real pandasHDFStoreexample with both series and a test through the ACS lane's own writer. Refusal:test_property_h5_layouts_the_reader_cannot_see_are_refusedtakes any such layout and adds one defect anywhere (avalues_block_*field at any position, a missing or displaced index field, a stray dataset, an untyped group, a series under an unknown key, a metadata key stored as a frame, a fixed frame with no column axis).requires_us). A frame written through the realPolicyEngineUSEngine().write_dataset, including register columns andwould_claim_wic, stores exactly the tables_export_stored_tablespredicts, and the gate and the written bytes refuse the same column. In production the post-write check enforces the weaker property that matters for certification: the written file's refused columns equal the gate's, or are empty when the gate could not evaluate. A writer that adds or drops a registered or variable column does not trip it. Unit tests cover each direction: the written file refusing more than the gate, refusing less (the gate refusedwould_claim_wicand the written file stores none), refusing different columns, an unevaluated gate with a clean and with a stale file, and a file or engine the check cannot read.would_claim_wicis refused by an engine that defines onlytakes_up_wic_if_eligible, and passes once the column is renamed. This holds for in-memory tables, a synthetic H5, the probe, the release tool's gate and the ACS local-area package stage (an H5 written by the lane's writer, refused with no release directory, then packaged once renamed)._main: in an otherwise green run, a stored-input refusal is the batched raise's only line and no H5 is written; a post-write premise failure aborts after the write, before any post-export scorer opens and before any manifest (thestored_input_refusedandstored_input_premisemodes oftest_main_writes_diagnostics_before_post_calibration_gate_failure). Structure, by AST: the gate assignment and the join of its failures are unconditional statements of_main's own body before the batchedif terminal_gate_failures:block; the write, the post-write check and itsraise RuntimeErrorare consecutive statements before the scorer. The probe calls the check beforeUSSingleYearDatasetis constructed, observed with fake country and wrapper modules and pinned by AST together with the receipt block.Mutation checks, run locally. Each mutant below was caught by the named tests and the file was restored.
if False:; the post-writeraisereplaced bypass. Each fails both the AST test and the matching_mainmode.values_block_*searched only in the first two fields; non-frame top-level objects skipped instead of refused; metadata keys skipped without the series check. Each fails the refusal property on its own.and column not in registerreplaced byand (column not in register or len(register) > 3)fails the refusal property deterministically and the monotonicity property in most seeds (a reviewer measured 29 of 30 fresh Hypothesis databases); the refusal property is the one that always catches it. The post-write equality weakened toset(refused) <= set(expected)failstest_the_post_write_check_reports_a_verdict_the_gate_did_not_reach(the gate refusedwould_claim_wic, the written file refuses nothing). An earlyreturn Nonewhen the gate did not evaluate failstest_the_post_write_check_grades_the_file_when_the_gate_could_notand the unevaluated case oftest_the_post_write_check_reports_a_file_it_cannot_grade.Round-2 review findings
medicare_part_b_premiumswas called a name no engine version defines. False: every version read from 1.452.0 to 1.670.2 defines it as an input, and every version read from 1.690.7 to 2.15.1 definesmedicare_part_b_premiums_reportedinstead. Reclassified as a retired engine input, with the version range stated as the versions read (above), in the register comment instored_inputs.py, the changelog, the README, both runbooks, two test comments and this body. The refusal message does not describe the column, so it is unchanged. Arequires_ustest now pins the replacement's shape in 2.2.1 (Person, YEAR, float, no formula).test_no_register_entry_rests_only_on_the_stale_releasescounted the refused ACS local release as live. Replaced bytest_only_the_acs_lane_spine_tags_rest_on_refused_files_alone(above).test_us_stored_input_register.pysaid no engine version defines theacs_keys. It now cites therequires_ustest that checks the installed engine.Split into a separate follow-up, not in this PR (tracked in #1034): recording the fiscal-refresh verdict and register digest in the release manifest or
gate_evidence(today it is in telemetry only); requiring thestored_inputsgate in_check_local_area_gates; and re-grading publisher compatibility-claim version ranges. On the last: the check grades only the policyengine-us version a release is certified against, so a compatibility claim that widens a source-enrichment release's range (up to the next major) is not re-graded for the other versions it admits.Round-1 review findings
{entity}_spine_source_id;acs_*andpuma_geoidunadjudicated. Registered all six spine source ids, the six ACS-nativeacs_*amounts andpuma_geoid, each bound to its producer, with the Build Q pool added to the evidence. The pool lanes now pass (table above)._populace_staging_metadataseries, registered the lane's*_spinetags, and narrowed the changelog, README and agent guide to name the three guarded seams. The lane's 2026-09-23 release is refused for the same two columns as its donor._check_compatibilitynow reports the mismatch first._mainmodes above and corrected invariant 6.Verification
71934da65andd35c43b0dchange comments, the changelog and a fixture'sroletext only. An independent claims review approved71934da65, and its P3s are fixed ind35c43b0d. The stored-input tests (92) pass at71934da65; CI results on the latest head are on this PR.Round 2 (the round-2 fixes, at
eea3aabfd; engine env: policyengine-us 2.2.1, policyengine-core 3.32.5):ruff check .:All checks passed!.ruff format --checkon the 4 Python files this round touched:4 files already formatted.python tools/ci_test_groups.py --verify:verification=ok(no test file added).test_stored_inputs.py: 69 passed.test_us_stored_input_register.py: 23 passed. Therequires_ussubset of the two is 11 passed (81 deselected); peak RSS 2.8 GB for the two files together.fast) lane was not rebuilt locally, because the machine is short on disk. Instead the same two files ran in the engine env with a pytest plugin that makespolicyengine_usandpolicyengine_coreunimportable and hides their distribution metadata: 81 passed, 11 skipped, every skiprequires policyengine-us extra. CI ran both files at0a460ced9in thefastrestgroup and in theengine-usus-amandus-qsgroups (all 24 checks green);eea3aabfdchanges only comments and the changelog.test_us_fiscal_refresh_builder.py: 315 passed, 2 skipped (both needMICROCOSM_US_CHRONICLE_FACTS), peak RSS 3.1 GB.git checkout: the register-size rule fails the refusal property in every run and the monotonicity property in most; the subset form of the post-write equality fails the(clean file, gate refused would_claim_wic)case; the earlyreturn Nonefailstest_the_post_write_check_grades_the_file_when_the_gate_could_notand the unevaluated case oftest_the_post_write_check_reports_a_file_it_cannot_grade.build_from_datasetreadsperson_{group}_role, elserole, before its variable loop; policyengine-us 2.2.1 routes an HDFStore path throughUSSingleYearDataset, which reads the top-levelperson,household,tax_unit,spm_unit,familyandmarital_unitframes, and passes aUSMultiYearDatasetinstance through unchanged.Round 1 (before the round-2 fixes):
ruff check .(the workspace's ruff 0.15.16):All checks passed!ruff format --checkon the 8 touched Python files:8 files already formattedpython tools/ci_test_groups.py --verify:verification=ok. No test file was added in this revision.uv sync --all-packages --extra us --locked; policyengine-us 2.2.1, policyengine-core 3.32.5): 30 test files, one pytest process per file, 2,097 passed, 9 skipped, 0 failed; peak RSS 5.2 GB (test_us_post_export_scoring.py). The 30 are every test file that namesbuild_us_fiscal_refresh_release,source_enrichment,stored_inputsorbuild_us_acs_local_release(27, found withgit grep), plustest_contract.py,test_publish_guard.pyandtest_us_release_gate_preflight.py. Among them:test_stored_inputs.py66 passed;test_us_stored_input_register.py17 passed;test_us_fiscal_refresh_builder.py315 passed, 2 skipped;test_us_acs_local_release_tool.py49 passed, 1 skipped (the pinned-feed test, which needsMICROCOSM_US_CHRONICLE_FACTS);test_source_enrichment.py140 passed;test_contract.py254 passed;test_us_multispine_pool_tool.py188 passed.uv sync --all-packages --locked, thefastlane's environment): the same 30 files, 1,979 passed, 87 skipped (engine-gated), 0 failed;test_us_medicaid_take_up.pycollects nothing without the engine. Peak RSS 1.5 GB.test_stored_inputs.py58 passed, 8 skipped;test_us_stored_input_register.py14 passed, 3 skipped;test_us_fiscal_refresh_builder.py309 passed, 8 skipped.would_claim_wicandmedicare_part_b_premiums_reported, and none ofmedicare_part_b_premiums,tax_unit_role_input,filing_status_input,puma_geoidor theacs_*names. In 2.2.1,medicare_part_b_premiums_reportedis defined and referenced nowhere else outside tests.Not verified here
policyenginewrapper package is not in theusextra, so neither this environment nor CI's engine lane can runrun_native_loader_compatibilityfor real. Its use of the check is tested through fake modules and AST ordering. The check itself runs against the real engine.stored_inputs.pyand did not touch those seams, sotest_source_enrichment.pyandtest_us_acs_local_release_tool.pywere not rerun; their round-1 results stand.🤖 Generated with Claude Code