Repository navigation
Conversation
A year file is named by its dataset's logical name, which drops the revision, so year files cut from one release of a dataset were reused after the bundle re-pinned the same name to another revision or sha256. Nothing recorded the policyengine-us or policyengine-core version that calculated the values either. The stamp (revision 2) now also records the source identity (the bundle URI and verified sha256, an explicit URI, or a local file's resolved path, size and mtime) and the installed package versions. ensure_datasets and load_datasets compare it with what the request resolves to now, computed from the manifest, the URI or one stat(), and read only the stamp. Fixes #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unit and Hypothesis tests extend the stamp's invariants (exactness, totality, named differences, ensure agrees with load, decides from the stamps alone) to the source identity and package versions. A new test file checks that identifying a request agrees with materializing it on every route, never reads the source, and changes exactly when the bundle re-pins an entry or a local file's size or mtime changes. Integration tests cover an edited local source, a re-pinned bundle dataset and a new policyengine-us version over the real engine. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k cycles as unreadable From the first review of #593: - create_datasets writes a dataset's year files, then checks the source again (dataset_source_change: re-hash a bundle file against its verified sha256, re-read a local file's size and mtime) and stamps them only if it still matches. A file replaced in a shared data folder after verification leaves the year files unstamped and raises DatasetMaterializationError. - Two requested datasets that are different sources with one stem would overwrite each other's year files and, with the identity check, be cut again on every call. ensure_datasets, load_datasets and create_datasets now refuse them with ValueError before anything is read or cut. - A soft-link cycle or overlong chain in place of the stamp or record made h5py raise RuntimeError out of year_file_stamp_problem; it now reads as unreadable content. - Docs: the certified default's own URI selects the bundle dataset, a local file is identified by file-system metadata, and create_datasets takes no lock. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
… before stamping From the second review of #593: an explicit URI's download shares the data folder's overwritable path, so another process could replace it before the cut and the year files would be stamped with the pinned URI anyway; a bundle file swapped out and back passed the re-hash. Now create_datasets records the source file's device, inode, size and mtime right after materializing it (dataset_source_snapshot) and stamps only if the file is still that one, for every kind of source, besides the bundle re-hash. A download replaces a file with a new inode. create_datasets' up-front check for shared year files now resolves requests with its own allow_unmanaged, so a request materialize_dataset would refuse is refused with the same error, without a network lookup. The changelog mentions both new errors. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d schemes early From the third review of #593 (APPROVE, low findings): - dataset_source_snapshot records size and mtime, not device and inode. Some FUSE mounts (sshfs without -o use_ino) renumber inodes, which would fail every cut; a replacement policyengine.py makes always writes a new file, so its mtime moves anyway. - requested_dataset_source_identity refuses a non-hf:// URI with materialize_dataset's "Unsupported explicit dataset URI" error, so create_datasets' up-front check refuses it before cutting anything. - The docs and docstring name the other gap: a file another process restores to the materialized size and mtime is not noticed. - Tests date materialized files in the past, so a replacement's mtime moves whatever the file system's timestamp resolution. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…RIs up front From the fourth review of #593: with device and inode out of the snapshot, an explicit URI replaced by a same-size download within one timestamp tick passed the check, and the docs claimed a replacement always moves the mtime. _download_hugging_face_dataset now hashes the bytes as it writes them (DatasetSource.downloaded_sha256), and dataset_source_change re-hashes a downloaded file against the sha256 it was verified against (bundle) or downloaded with (explicit URI). The remaining, documented gap is a local file changed within one tick at the same size, or restored to its size and mtime. requested_dataset_source_identity parses an hf:// URI as materialize_dataset does (shared _parse_hugging_face_uri), so a malformed one is refused before anything is cut. The stamp tests' fixture URI gains its repository segment; the refusal tests assert DatasetMaterializationError. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the fifth review of #593: the re-hash checks the file when the year files are stamped, not as it is read, so a source swapped out while it loads and then restored exactly is not noticed for downloaded files either. Only the same-size change within one timestamp tick is limited to local files. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
create_datasets with no years cuts and stamps nothing, so it no longer stats or re-hashes the source. tests/test_dataset_runtime.py mocks materialize_dataset with a path that does not exist and calls create_datasets(years=[]); it failed with FileNotFoundError from the snapshot. Reported by the UK year-file session (#600). The full suite passes locally: 1543 passed, 9 skipped. Refs #591 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mirror #593's c7bb48d: create_datasets with no years snapshots and re-checks nothing, so the UK runtime tests keep their original mocked paths. Docs say moving a year file into place needs write access to the data folder and gives the file the cutting user's mode and group. The mode assertions skip on Windows, and the float-year test runs ensure and load after the cut. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MaxGhenis, two things from a local merge check of #593 against
|
Fixes #591
The defect
ensure_datasets()names each US year file{data_folder}/{stem}_year_{year}.h5, with the stem fromdataset_logical_name(resolve_dataset_reference("us", dataset)). That drops the@revision, andcreate_datasets()uses the same stem (materialize_dataset(...).name). Since #590, a year file is reused when its stamp holds this release's register of renamed inputs and format revision. Nothing in the stamp says which source the file was cut from, or under which engine:populace_us_2024_acs_local, whose replacement (Publish complete ACS-local WIC participation and remove the temporary .py assumption microcosm#1154) ends the temporary ACS WIC assumption (Remove temporary ACS WIC assumption after replacement certification #563).allow_unmanaged=True) keeps its year files. Two local files with the same name in different folders share year files.create_datasetsstores each input assim.calculate(variable, period=year)returns it. policyengine-core 3.33.0 uprates an input from its stored year by the policyengine-us parameter named in the variable'suprating(Simulation._calculate; 367 policyengine-us 2.2.1 variables set one, for exampleemployment_income_before_lsr→calibration.gov.irs.soi.employment_income). So year-file values depend on both versions, and neither was recorded.What changes
The stamp, revision 2 (
legacy_inputs.YEAR_FILE_STAMP_REVISION1 → 2). Example for the certified default:{"packages": {"policyengine-core": "3.33.0", "policyengine-us": "2.2.1"}, "register": {"would_claim_wic": "takes_up_wic_if_eligible"}, "revision": 2, "source": {"sha256": "6496cc43…", "uri": "hf://policyengine/populace-us/populace_us_2024.h5@populace-us-2024-spm-20260915"}}sourceisdataset_materialization.dataset_source_identity(materialize_dataset(...)), which has one of three shapes:{"uri", "sha256"}, the pinnedhf://…@revisionURI and the sha256 thatmaterialize_datasetverified the local copy against;{"uri"}, as given;{"path", "size", "mtime_ns"}, from onestat()of the resolved path, taken before the Microsimulation loads it.packagesislegacy_inputs.installed_package_versions():importlib.metadataversions ofYEAR_FILE_PACKAGES = ("policyengine-core", "policyengine-us"), withNonewhen one is not installed. No package is imported.The check.
year_file_stamp_problem(path, source, packages=None)returnsNoneonly when the stamp equalsyear_file_stamp(source, packages)exactly, and the renames record lies within the stamped register (as before).ensure_datasetsandload_datasetspassrequested_dataset_source_identity("us", dataset), which mirrorsmaterialize_dataset's routing without downloading or reading anything: the bundle manifest entry, the URI, or file-system metadata. The reason names what differs: the renames (as before), both sources, and each package version that changed. For example:Guards around the cut.
create_datasetsrecords the source file's size and mtime right after materializing it (dataset_source_snapshot). Once a dataset's year files are all written, it stamps them only ifdataset_source_changefinds:_download_hugging_face_datasetnow hashes as it writes;DatasetSource.downloaded_sha256).This catches another process that materializes another release, or another file with the same name, into a shared data folder. That includes a same-size download landing within one timestamp tick, and the pinned release downloaded back afterwards. On a change, the year files are left unstamped, so they are cut again, and
DatasetMaterializationErroris raised.Two gaps are documented:
Device and inode are deliberately not compared: some FUSE mounts renumber them, which would fail every cut.
Up-front refusals.
requested_dataset_source_identityrefuses whatmaterialize_datasetwould refuse, with the same error: unsupported schemes (s3://), malformedhf://URIs (a shared_parse_hugging_face_uri), and, fromcreate_datasets, unmanaged requests withoutallow_unmanaged. So a multi-datasetcreate_datasetsfails before cutting any of them. Two requested datasets that are different sources with one stem, such aspop.h5@r1andpop.h5@r2, would overwrite each other's year files, and with the new check they would be cut again on every call.ensure_datasets,load_datasetsandcreate_datasetsrefuse them withValueErrorbefore anything is read or cut.HDF5 links. A soft-link cycle, or a chain too long to follow, in place of the stamp or the renames record made h5py raise
RuntimeErrorout ofyear_file_stamp_problem. This was inherited from #590. It now reads as unreadable content.Year files with a revision-1 stamp are cut again once. Revision 1 exists only on
mainsince #590, which has not been released (6.3.2 is the latest tag).Decisions
rsyncmakes by default. Touching or copying the file triggers a recut, which errs on the safe side. A rewrite that keeps both size and mtime (touch -r, or a rewrite within the file system's timestamp resolution) is missed. That blind spot is documented, and a property test labels it as intended.usextra (==3.33.0,==2.2.1), so they move only when policyengine.py moves a pin, and core's uprating produces the values. An editable install keeps its version string while its code changes, which the docs note.legacy_inputs, where Stamp US year files with the rename register they were cut under #590 put it. The source identity lives inprovenance/dataset_materialization.py, next tomaterialize_dataset, so the UK year files could reuse it.Invariants (all executed)
true≠1and1.0≠1) and its record lies within its register.test_a_year_file_is_current_only_under_the_exact_stamp_requested(400 examples; sources, versions and registers drawn from small alphabets, so near misses are common)test_a_year_file_cut_from_another_source_names_both,test_a_year_file_cut_with_other_versions_names_them,test_a_year_file_stamped_by_revision_1_is_cut_again_onceNone, and raises only for permission, directory or lock errors;Noneexactly for the requested stamp, however the nodes are stored.test_the_year_file_problem_is_total_and_exact_for_any_content(400 examples, including well-formed keys with wrong-typed values; both directions, with an independent decoder for raw text and bytes)test_a_malformed_stamp_is_out_of_date(26 cases),test_links_that_cannot_be_followed_are_unreadable_content(cycle and long chain, stamp and record)test_a_stale_year_file_names_each_source_and_version_that_differs(300),test_a_stale_year_file_names_each_rename_it_may_lack(200)test_every_difference_is_named_in_orderrequested_dataset_source_identity(dataset)equalsdataset_source_identity(materialize_dataset(dataset, allow_unmanaged=True))on every route.test_a_request_is_identified_as_what_it_materializes(12 routes: default, default URI, bundle name, bundle name in another repo, explicit URI with or without a revision, explicit URI of a bundle dataset, data-release name, and local absolute, relative,~and symlinked paths)test_a_bundle_dataset_is_re_identified_exactly_when_re_pinned(150),test_a_local_file_is_re_identified_when_its_size_or_mtime_changes(150; equal size and mtime over other bytes is the labelled, intended blind spot)test_a_local_file_elsewhere_is_another_source,test_re_pointing_a_symlink_re_identifies_the_sourcetest_ensure_decides_from_the_stamps_alone(8 cases, bundle-free URI and local source),test_identifying_a_request_never_reads_the_source(5 routes)ensure_datasetscuts again exactly when a file is missing orload_datasetswould refuse it, logging each stale file with its reason.test_ensure_cuts_again_exactly_when_load_would_refuse(80; 19 states, now including another source revision, a verified source, other core/us versions and revision 1)test_a_replaced_explicit_download_has_changed_whatever_its_metadata(100),test_a_local_source_has_changed_exactly_when_its_size_or_mtime_has(100; same size and mtime over other bytes is the labelled gap)test_a_source_replaced_after_its_snapshot_has_changed(bundle, explicit URI, local),test_a_downloaded_file_replaced_within_one_timestamp_tick_has_changed,test_a_bundle_file_swapped_out_and_back_has_changed,test_a_bundle_file_replaced_before_its_snapshot_has_changed,test_an_explicit_download_records_the_sha256_of_its_bytes,test_a_source_replaced_while_it_is_cut_leaves_the_year_files_unstamped(bundle, explicit URI and local, real engine),test_create_datasets_stamps_the_year_files_after_saving_and_checkingtest_two_sources_with_one_stem_are_refused_before_anything_is_cut(60)test_the_collision_refusal_names_both_requests_and_sources,test_one_source_requested_by_two_names_is_not_a_collision,test_create_refuses_what_materialize_would_with_its_error,test_create_refuses_a_uri_materialize_would_before_cutting_anything,test_a_uri_materialize_refuses_is_refused_with_its_errortest_editing_a_local_source_cuts_its_year_files_again,test_a_re_pinned_bundle_dataset_cuts_its_year_files_again(the ACS-local replacement in miniature: a fixture bundle re-pins a logical name, no network),test_another_package_version_cuts_year_files_againMutation testing
Mutation testing used 26 distinct mutants, each applied in a throwaway worktree and run against the tests. All 26 were killed, including both mutants of
create_datasets, which only the real-engine integration tests catch. The mutants cover the parts of the change that load-bearing behaviour depends on:boolsize; accepting any source as well formed.installed_package_versionsreturning nothing; dropping the mtime's nanoseconds from the description.hf://parse; letting a link cycle escape.Logs and the harness are in
~/PolicyEngine/_reviews/pr593-year-file-source-identity/on the build machine.Review
GPT-6.1 Sol reviewed this PR six times through Subfleet. Each round's findings were fixed or explicitly accepted:
s3://passing the up-front check, and documentation of a restore gap.hf://URIs also passed.git diff --stat.create_datasetsnow snapshots and re-checks the source only when year files are cut (yearsnon-empty).test_dataset_runtimemocks a nonexistent bundle path withyears=[], and it failed on the snapshot before this. The UK year-file session found the failure.A sibling review of #594 found that the totality oracle took a raw node's descriptor rather than its decoded value. That is fixed here too.
Accepted, not changed:
ensure_datasetsenforcesallow_unmanagedonly when it has to cut, so existing year files for an unmanaged source still load without the flag. This predates this PR and is unchanged by it.managed_microsimulationcould recorddownloaded_sha256in its run provenance. That is left for a follow-up.Verification
pytest testssuite passed locally: 1543 passed, 9 skipped (21 min). The integration tests use tiny fixtures; no full-population Microsimulation is built.ruff format --check .andruff check .are clean.mypyreports nothing new in the changed modules, compared withorigin/mainunder the same configuration.Related
populace_us_2024_acs_local, year files cut from the old pin are cut again by this PR without a new revision. Year files cut from the old dataset requested by its explicit URI still need Remove temporary ACS WIC assumption after replacement certification #563 to raiseYEAR_FILE_STAMP_REVISION, as its comment says.legacy_inputs.py,docs/microsim.mdandtests/test_us_year_file_stamp.py. Their sessions plan to land first. This PR will then rebase and adapt them:year_file_stamp()forhf://fixture/{stem}.h5. That needs a source argument and a well-formed URI.output_stamp_problemwill accept a recorded year-file stamp exactly whenyear_file_stamp(stamp["source"]) == stamp. That is the rule agreed with its session.uk/datasets.py) record only their data year, so they have the same gap. That is left for a follow-up.axiom: n/a: year-file caching infrastructure, no policy change.
Country package versions are unchanged.
🤖 Generated with Claude Code