Repository navigation
Conversation
…under A US run records, before applying the legacy-input mapping, a stamp holding the whole LEGACY_INPUT_RENAMES register, an output format revision and the year-file stamp of its input dataset (null when it keeps none). save() writes it last as policyengine_output_stamp; load() refuses an output whose stamp is not exactly this release's, so Simulation.ensure() calculates it again. PolicyEngineUSDataset.load() and create_datasets() keep a year file's stamp in the dataset's metadata so a run can record it. Fixes #592. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e agreement Engine-free unit and Hypothesis tests state exactness, totality, round trip, named renames, truncation and stamp distinctness for output_stamp_problem. Integration tests over policyengine-us 2.2.1 with the three-person WIC fixture replay the WIC history for a saved output, cover year-file provenance, save ordering and partial saves, and check that Simulation.load() refuses, and ensure() runs again, exactly when output_stamp_problem gives a reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The shared H5 JSON reader treats RuntimeError, which h5py raises for a soft link that leads back to itself and which covers RecursionError, as unreadable content, so output_stamp_problem, year_file_stamp_problem and year_file_stamp_metadata return a reason rather than raise. - The totality property decodes raw nodes independently and asserts both directions; the exactness generator weights with sampled_from, since st.one_of drops repeated branches. - A region-scoped run records the year-file stamp of the dataset it was given, tested over a current and an out-of-date year file. - The changelog no longer overstates the stale-year-file guarantee. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
added a commit
that referenced
this pull request
Oct 13, 2026
The totality property checked only that a None result was current, and gave the oracle a raw node's descriptor rather than what the checker reads back. A current stamp with an empty record stored as raw text or bytes is current, but the oracle called it out of date. _stored decodes raw nodes independently of the checker (fixed-length bytes read back without trailing NULs), and the property now asserts exactness in both directions. Found by the #594 review in the sibling oracle. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 13, 2026
This branch has not been deployed
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 #592
Follow-up to #590 (issue #532), which stamped
create_datasetsyear files with the rename register and left saved simulation outputs out of scope.The defect
MicrosimulationModelVersion.load()refused a saved US output only when it had no renames record (policyengine_legacy_input_renames), andsave()required only that record. The record shows that an output was calculated with the legacy-input mapping, not under which register. OnceLEGACY_INPUT_RENAMESgains, loses or retargets an entry, outputs saved earlier still carry a record, soSimulation.load()andSimulation.ensure()reuse them. An output calculated before the WIC entry existed records{}and passed that check, though every WIC-eligible person in it took WIC up (PolicyEngine/microcosm#1026).There was a second gap. A run over a year file maps only what the file still stores under a legacy name. So an output also depends on the register the year file was cut under, and on its format revision, and nothing in the output recorded either. An output calculated from a stale year file opened directly (
PolicyEngineUSDataset(filepath=...), whichload_datasetsrefuses) was reused indefinitely. Outputs over year files would also be reused afterYEAR_FILE_STAMP_REVISIONis raised, for example when #563 removes the temporary ACS WIC assumption.What changes
The stamp. A US
run()records, before it applies the mapping,metadata["output_stamp"]=legacy_inputs.output_stamp(year_file):{"register": {"would_claim_wic": "takes_up_wic_if_eligible"}, "revision": 1, "year_file": null}registeris the wholeLEGACY_INPUT_RENAMES, read when the run starts, as for year files.revisionis the newOUTPUT_STAMP_REVISION. Its comment says to raise it withYEAR_FILE_STAMP_REVISIONwhen the temporary ACS WIC assumption is removed, because a run straight over the ACS source applies that assumption too.year_fileis the stamp of the year file the input data was cut into, ornullwhen the input dataset keeps none (in-memory data, a source file). This answers the "carry the year file's stamp into the output's provenance" question: yes, because the output inherits whatever the year file lost.Where the year-file stamp comes from.
PolicyEngineUSDataset.load()keeps a file's year-file stamp inmetadata["year_file_stamp"], and drops the key when the file has none.create_datasets()sets the key on the datasets it returns, so a run overensure_datasets()output records it on both the load and the create paths.legacy_inputs.year_file_stamp_metadata(path)reads the stamp. It returnsNoneonly when no stamp is stored. A stamp stored as JSONnull, or one that cannot be read, becomes the string"unreadable", so a corrupt year-file stamp can never pass as "no year file".Save.
MicrosimulationModelVersion.save()requires the recorded stamp, as it already required the record. It writes the stamp last, as H5 datasetpolicyengine_output_stamp, after the tables, the record and the SPM receipt. It writes the stamp the run recorded, not one made at save time.PolicyEngineUSDataset.save()truncates the file first, so a save that stops part-way leaves no stamp.Load.
load()callslegacy_inputs.output_stamp_problem(path)first and raisesValueError("Saved US simulation output {path} is out of date: {reason}. Run the simulation again (Simulation.ensure() does so).").Simulation.ensure()already reruns on any load error. A missing file still raisesFileNotFoundError, soensure()reruns without logging an unexpected error.load()restores the stamp into metadata, so saving a loaded output again keeps it.The check. It mirrors
year_file_stamp_problem. An output is current only when all of these hold:register,revisionandyear_file;revisionis anint(not abool) equal toOUTPUT_STAMP_REVISION;registeris a str→str map equal to this release's;year_fileisnull, or is exactly this release'syear_file_stamp(), compared with the same strict rules (trueis not1, and1.0is not1);Put another way, the stamp equals what this release would write for a run over a dataset with no year-file stamp, or over a current year file. The reasons come from the register alone, with no WIC special case, and name each disagreeing entry as
old -> new. For example:Distinct stamps. The output stamp's H5 name and key set both differ from the year file's. So an output never passes as a year file, nor a year file as an output, even when one stamp is stored under the other's name.
The
_register_differenceshelper is factored out ofyear_file_stamp_problem, with no change to its behaviour (all 71 year-file tests pass unchanged).Invariants
All of these are executed, not just stated. The Hypothesis oracles are stated on the stored JSON (and, for raw nodes, on an independent decoding of how HDF5 stores them), not by calling the code under test.
year_fileisnullor exactly this release's year-file stamp, and its record lies within the stamped register. Equivalently, the stamp equalsoutput_stamp(None)oroutput_stamp(year_file_stamp()).test_an_output_is_current_only_under_this_releases_exact_stamp(300 examples, weighted so about 44% are current, 22% are stale only through their year file, and 30% are stale otherwise; registers come from a four-name alphabet)test_a_future_register_entry_…,test_retiring_or_retargeting_an_entry_…,test_raising_the_output_revision_…,test_raising_the_year_file_revision_makes_outputs_from_year_files_out_of_dateNoneand agrees with the oracle in both directions. It raises onlyFileNotFoundErrorand the three errors unrelated to content.test_the_output_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(16 cases), the out-of-date year-file cases (15), raw-node records, cyclic soft links, dangling links, deep JSON, forcedRecursionError, and errors raised on open and on readtest_an_output_saved_before_the_mapping_is_not_reused(existing test, now with record and stamp both removed)test_an_output_stamp_reads_back_as_written(50 examples)test_the_stamp_is_stored_as_utf8_json,test_a_saved_output_is_stamped_with_the_register_it_was_calculated_underold -> new, and no entry they agree on.test_a_stale_output_names_each_rename_it_may_lack(200 examples over the output's register and its year file's register together)test_a_truncated_output_is_never_current(100 prefix lengths)test_save_stamps_the_output_last(call order, and the record and SPM receipt already present when stamping),test_an_output_whose_save_stopped_before_the_stamp_is_calculated_again,test_an_output_without_a_stamp_is_not_saved,test_saving_the_tables_again_drops_the_stamptest_save_writes_the_stamp_the_run_recorded,test_a_run_over_a_year_file_records_its_stamp(both the create and load paths),test_a_region_scoped_run_records_its_year_files_stamp,test_a_dataset_keeps_its_year_files_stamp(11 stored forms),test_reloading_a_resaved_year_file_drops_the_stamp_it_kepttest_an_output_never_passes_as_a_year_file_nor_the_reverse(100 examples)test_an_output_is_never_loaded_as_a_year_file_nor_the_reverse(Simulation.load()on a year file, andload_datasetson an output, over the real engine)Simulation.load()refuses exactly whenoutput_stamp_problemgives a reason, and its message carries that reason.ensure()runs again exactly then, and otherwise reuses the output.test_load_refuses_and_ensure_runs_again_exactly_when_an_output_is_out_of_date(60 examples over 3 registers × 14 stamps × 8 records, applied to a real saved output; about 30% are current)test_the_decision_reads_only_the_stamp_and_the_recordIntegration over the real policyengine-us 2.2.1 engine uses the three-person Los Angeles WIC fixture.
test_an_output_calculated_before_a_register_entry_is_calculated_againreplays the WIC history with no fake engine. It runs and saves under an empty register: the infant gets WIC, and the output records{}, which the old check accepted. Under the real register,load()refuses it with a reason namingwould_claim_wic -> takes_up_wic_if_eligible.ensure()runs again, the infant's WIC is 0, and the nextensure()reuses the output.test_an_output_from_an_out_of_date_year_file_is_not_reusedcuts a year file under an empty register and opens it directly. The run over it is calculated under the current register but gives the infant WIC, andload()refuses it on the year-file reason alone. Afterensure_datasetscuts the file again,ensure()over it recalculates, and the infant's WIC is 0.test_a_future_register_entry_makes_saved_outputs_staleadds a fictitious entry that the engine does not define. Outputs go stale and are calculated again with unchanged results, and they go stale once more when the register reverts.Mutation testing
All 44 mutants were killed. They ran in three parallel copies of the source, with the Hypothesis database cleared before each mutant. Unit tests ran first, and the engine integration tests ran only for a mutant the unit tests left alive. The harness (
mutate.py), the per-worker logs andmutation-summary.txtare in~/PolicyEngine/_reviews/pepy-592/. Each line of the summary names the first test that failed.output_stamp_problem:isinstancerevision acceptingTrue;{};year_filekey optional;isinstancerevision;==comparison, which acceptstruefor1.RecursionErrorcaught).nullyear-file stamp kept asNone;None;year_file_stamp_metadata;datasets.py:load()keeping no year-file stamp;load()not dropping a stale one;create_datasetskeeping none;load()reading throughread_year_file_stamp, which conflatesnullwith absent.model.py: the run recording no year file; a scoped run reading the scoped copy's empty metadata.model_version.py:save()stamping with the register at save time;save()not stamping;load()ignoring the check, or keeping the record-only check;load()not restoring the stamp.Review
Three independent reviews ran on read-only Subfleet lanes. Their reports are in
~/PolicyEngine/_reviews/pepy-592/.GPT-6.1 Sol (hard tier) at
66e97bb4: APPROVE (review-1.md), with three minor findings and a nit, all fixed ine7261779:RuntimeErrorinstead of reading as unreadable content. The fix is in the shared JSON reader, so year files gain it too;("text", "{}"), which decode to a valid record. It now decodes them independently and asserts both directions;It also noted that repeated
st.one_ofbranches don't weight a draw. Hypothesis drops duplicate branches, which I confirmed. The generator now weights withsampled_from.Opus 5.5 on
66e97bb4..e7261779: APPROVE (review-2.md; no Codex lane was free). Both of its nits, an exact-reason assertion and a comment, were fixed in90fa5037.Opus 5.5 on
e7261779..a51f31c0: APPROVE (review-3.md). This delta adds the nits andstr(...)around the output path, which removes a new mypyarg-typeerror and cannot change behaviour. Its optional comment nit was fixed inf06017bc, a comment-only commit.1e107e09mergesorigin/main(48ba01a5, a version bump).Not covered here
storage_idis not content-addressed and carries no model or data version (Simulation cache is write-only: uuid4 keys mean ensure() never reloads results, and data_folder accumulates orphaned h5 files #455). The stamp makes outputs stale when the register changes, not when the engine or the data does.dataset_logical_namedrops@revision, so a year file re-cut from a new release of the same logical dataset carries the same stamp. That is the separate follow-up Stamp US year files with the rename register they were cut under #590 named.nulland is reused.load_datasetsrefuses such files, andensure_datasetscuts them again.release_bundleare unchanged. A run record already binds the SHA-256 of the input and output files, and those bytes include both stamps.OUTPUT_STAMP_REVISIONas well asYEAR_FILE_STAMP_REVISION. Both constants' comments say so.What I verified
Locally with Python 3.14.4 on macOS,
HF_HUB_OFFLINE=1, no downloads. The host was under heavy load (load average 50–80).e7261779:tests/test_us_output_stamp.py: 105 passed;tests/test_us_year_file_stamp.py: 71 passed, unchanged by the_register_differencesrefactor;tests/test_us_legacy_inputs_integration.pyandtests/test_us_legacy_inputs.py: 119 passed, peak RSS 4.0 GiB.66e97bb4, 154 passed in the related suites:test_cache,test_dataset_persistence,test_run_record,test_spm_model,test_small_follow_ups,test_us_native_alignment,test_us_long_term_datasets,test_extra_variables,test_us_microsim_structural_reforms,test_us_program_statistics,test_poverty_run,test_uk_year_file_data_yearandtest_release_manifests. Peak RSS was 7.1 GiB.a51f31c0;f06017bc;1e107e09only merges the version bump.format --checkandcheckare clean. mypy (informational in CI) reports no new errors in the changed modules compared withorigin/main.CI: pending at
1e107e09(the runs queued behind a runner backlog; earlier heads were cancelled by later pushes).axiom: n/a: infra (US saved-output cache staleness; no policy logic)
🤖 Generated with Claude Code