Repository navigation
Stamp US year files with the rename register they were cut under - #590
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…evision bump Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ualified claims - Treat JSON nested past the recursion limit as unreadable content - Raise permission, directory and lock errors from reads, not just opens - Let the decision test read only the stamp and the record - Pin fixed-length strings, a raised revision and exact log lines - Stop the reasons and docs asserting history the code cannot know Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g links Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tmp_path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CPython 3.14 on Linux parses 100,000-deep JSON into nested lists instead of raising RecursionError, as 3.11-3.13 and 3.14 on macOS do. The check gives the same reason either way; a new test forces RecursionError so that path is covered on every platform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 13, 2026 08:11
Contributor
Author
|
Merged at head
Follow-ups proposed as separate sessions:
|
This was referenced Oct 13, 2026
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.
Fixes #532
The defect
ensure_datasets()reuses a US year file ({data_folder}/{stem}_year_{year}.h5) whenever it exists and passes its staleness check. #531 made that check "does the file carry the renames record?" (policyengine_legacy_input_renames). The record shows that a file was cut with the legacy-input mapping, not under which register. #531's own body and the comment onRENAMES_H5_DATASETboth say so. OnceLEGACY_INPUT_RENAMESgains an entry (policyengine-us renames another input), year files cut before it still carry a record and are reused. They can lose the newly renamed input exactly as files cut by policyengine.py 6.1.1 and earlier lost the WIC take-up draw: about $11.5B of WIC instead of $6.6B in 2024 (PolicyEngine/microcosm#1026).A subset rule ("the file's register covers today's") would not close the gap either:
_build_simulation_from_datasetsets only columns insystem.variables.What changes
The stamp. Each year file
create_datasets()writes now ends with an H5 dataset,policyengine_year_file_stamp(legacy_inputs.YEAR_FILE_STAMP_H5_DATASET). It holds UTF-8 JSON{"register": <the whole LEGACY_INPUT_RENAMES>, "revision": 1}:PolicyEngineUSDataset.save()openspd.HDFStore(mode="w"), which truncates the file, then writes the tables and then the renames record. Only after that is the stamp written. A write that stops at any point leaves no stamp, so the file is cut again.YEAR_FILE_STAMP_REVISIONinvalidates every year file whenever extraction changes in a way the register cannot express. Its comment names one such change that is coming: removing the temporary ACS WIC assumption from Temporarily handle missing ACS WIC participation inputs #566 (planned in Remove temporary ACS WIC assumption after replacement certification #563). Year files cut while that assumption applies store assumed claiming astakes_up_wic_if_eligible.save()neither writes nor keeps the stamp. Re-saving a year file drops it, so the file is cut again.The check.
legacy_inputs.year_file_stamp_problem(path)returnsNoneonly when all of these hold:registerandrevision;int(notbool) equal toYEAR_FILE_STAMP_REVISION;Otherwise it returns a reason clause, built from the register, with no WIC special case. It reads only those two H5 datasets, never the tables or the source dataset. Any other
OSErrorwhile opening or reading, or content that cannot be parsed (including JSON nested past the recursion limit), counts as out of date. APermissionError,IsADirectoryErrororBlockingIOErroris raised instead, because cutting the file again would only fail on the write.Callers.
ensure_datasets()checks every year file and logs each out-of-date one with its reason. It then cuts all of them again, as before.load_datasets()raisesValueError("US year file {path} is out of date: {reason}. Regenerate it with ensure_datasets() or create_datasets().").read_renames_record()now returnsNonefor a malformed record (a JSON list, non-string values, bad JSON, a group) instead of accepting[]as{}or raising.Examples of the reasons:
Files written under #531 carry the record but no stamp, so they are cut again once. Grandfathering them would mean hard-coding #531's register, which is a WIC special case.
Invariants
All of these are executed, not just stated.
test_a_year_file_is_current_only_under_this_releases_exact_stamp(300 examples; registers drawn from a 4-name alphabet, so subsets, supersets and retargets are common)test_a_future_register_entry_makes_a_stamped_file_out_of_date,test_retiring_or_retargeting_an_entry_…,test_raising_the_format_revision_makes_older_files_out_of_dateNone. It raises only the three errors unrelated to content.test_the_year_file_problem_is_total_and_exact_for_any_content(300 examples: arbitrary JSON, raw text, raw bytes, numbers, arrays, groups)test_a_malformed_stamp_is_out_of_date(14 cases),test_json_nested_too_deeply_is_out_of_date_not_an_error,test_a_recursion_error_while_parsing_is_unreadable_content,test_errors_unrelated_to_the_contents_are_raisedand…_while_readingread_renames_record(path) == recordwhenever current)test_a_year_file_stamp_reads_back_as_written(50 examples)test_the_stamp_is_stored_as_utf8_json,test_a_stamp_and_record_stored_as_fixed_length_strings_are_readold -> new, and no entry they agree on.test_a_stale_year_file_names_each_rename_it_may_lack(200 examples)test_the_refusal_names_the_file_and_what_it_may_lacktest_a_truncated_year_file_is_never_current(100 prefix lengths)test_a_save_that_stops_part_way_leaves_no_stamp,test_saving_a_year_file_again_drops_its_stamp,test_create_datasets_stamps_each_year_file_after_saving_it(call order),test_a_year_file_whose_stamp_was_never_written_is_out_of_dateensure_datasetscuts again exactly when a file is missing orload_datasetswould refuse one, logs exactly the out-of-date files with their reasons, and reuses the results.test_ensure_cuts_again_exactly_when_load_would_refuse(60 examples over 14 file states × 1–3 years)test_ensure_decides_from_the_stamps_alone(patchesh5py.Fileso only the stamp and record can be read, and makespd.HDFStoreandmaterialize_datasetraise)Integration over the real policyengine-us 2.2.1 engine uses the three-person Los Angeles WIC fixture.
test_a_year_file_cut_before_a_register_entry_is_regeneratedreplays the WIC history with no fake engine. It cuts under an empty register, and the file stores neither name, so the infant gets WIC. Back under the real register,load_datasetsrefuses the file, namingwould_claim_wic -> takes_up_wic_if_eligible.ensure_datasetscuts it again, and the infant's WIC is 0.test_a_future_register_entry_regenerates_year_filesadds a fictitious entry whose names the engine does not define. The file goes stale and is cut again, the data is unchanged, and the new stamp carries the entry.test_ensure_datasets_regenerates_a_year_file_with_only_the_renames_recordcovers a file written under Map the stored WIC take-up draw onto takes_up_wic_if_eligible when loading US data #531.test_ensure_datasets_regenerates_a_year_file_cut_before_the_mappingstill passes unchanged.Mutation testing. All 32 mutants were killed. 30 ran through a harness on a separate copy of the source, with the Hypothesis database cleared before each mutant. Two were run by hand: a table read in the predicate, and
pd.read_hdfin the decision. Each was killed by the test aimed at it:<instead of!=,isinstanceacceptingTrue, hardcoded1.{};RecursionErrornot caught.read_renames_recordaccepting lists;create_datasetsandsave(): no stamp written; stamp written beforesave();save()appending instead of truncating.ensure_datasetsandload_datasets:ensure_datasets, and separatelyload_datasets);load_datasetsskipping existing files;pd.read_hdfduring the decision.The harness and its log are in
~/PolicyEngine/_reviews/pepy-590/(mutate2.py,mutate2.log). Two mutants can't be killed, as the review noted: thenode.shape != ()guard is equivalent, andKeyErrorin the reader's catch is defensive, since dangling links read as absent.Review
An independent GPT-6.1 Sol review (hard tier) asked for changes. The review is in
~/PolicyEngine/_reviews/pepy-590/review-1.md. Every finding was fixed in10201bf8:RecursionErroron deeply nested JSON;The one exception is the source-revision finding, which is a separate follow-up (see below). A delta review (served by Opus 5.5 when no Codex lane was free) approved
9cfc1ed2(review-2.md). Its three nits:96a7e974. The decision test now also blockspandas.io.pytables.HDFStoreandtables.open_file. A mutant addingpd.read_hdf(filepath, "person")to the decision is now caught, which I confirmed.node.shape != ()guard is an equivalent mutant, kept as documentation.A third review, of
9cfc1ed2..96a7e974(Opus 5.5,review-3.md), approved with two optional test nits. Both were applied in69296e0e, as it prescribed:HDFStorepatches now use a raising class, so pandas'isinstancechecks still work and apd.read_hdfmutant fails with the intended message, which I confirmed;tmp_path.A fourth review, of
96a7e974..318e109d(GPT-6.1 Sol,review-4.md), approved the platform fix below. It confirmed both parameters of the forced-RecursionErrortest fail when the catch is removed, and that the temporary globaljson.loadspatch and the nested read-only HDF5 opens are safe. Its one nit was that the comment gave the macOS/Linux split as a rule when it is observed behaviour. That was fixed inf0db8f31, a comment-only commit.Not covered here
Simulation.load()still gates only on the record. The comment onRENAMES_H5_DATASETand docs/microsim.md say so. This is proposed as a separate session.dataset_logical_namedrops@revision, so year files cut from one release of a dataset are reused after the bundle re-pins the same logical name. This is proposed as a separate session. It matters before the ACS-local replacement (microcosm#1154, Remove temporary ACS WIC assumption after replacement certification #563).ensure_datasetscalls on one folder are not locked, as before. A crash mid-write loses the old file, which is then cut again; it is not replaced atomically.What I verified
Locally with Python 3.14.4 on macOS,
HF_HUB_OFFLINE=1, no downloads, peak RSS 3.7 GiB:10201bf8,tests/test_us_year_file_stamp.py,tests/test_us_legacy_inputs.pyandtests/test_us_legacy_inputs_integration.py: 174 passed. Later commits change onlytests/test_us_year_file_stamp.py, whose 71 tests pass at318e109d(f0db8f31changes only a comment);test_dataset_*,test_release_manifests,test_run_record,test_us_long_term_datasets,test_us_native_*): 139 passed;format --checkandcheckclean.CI at
69296e0efound a platform difference. CPython 3.14 on Linux parses the test's 100,000-deep JSON into nested lists instead of raisingRecursionError, as 3.11–3.13 and 3.14 on macOS do. The check gave the right reason either way: a list is a malformed stamp. Only my test's expectation ofread_year_file_stamp(...) is Noneassumed the parser fails.318e109dmakes that test hold either way, and adds a test that forcesRecursionError, so the guard is covered (and its mutant caught) on every platform.CI: pending on the latest head.
axiom: n/a: infra (US year-file cache staleness; no policy logic)
🤖 Generated with Claude Code