Repository navigation
Conversation
The immigration stage now emits years_since_us_entry for ASEC persons: period minus an arrival year drawn (keyed blake2b, within the vintage's PEINUSYR band and bounded by birth year) for the foreign-born, and age for the US-born, matching #1052. ACS rows receive it through the same transfer as their immigration status. Registries, pins and generated artifacts are updated for the new column; a composition summary and gate cover the clock. Fixes #776 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LqCGPgxDukji8Bhs57c5rH
MaxGhenis
added a commit
that referenced
this pull request
Nov 3, 2026
The generator now names the table to change when a numeric pool input declares an engine unit it cannot map, or no unit: _CATALOG_UNIT_BY_ENGINE_UNIT and the schema's unit enum, or _REVIEWED_UNIT_BY_UNITLESS_INPUT. Open drafts that add such inputs hit these errors when they rebase. In #1085, years_since_us_entry declares unit "year" in policyengine-us 2.2.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring #1085 (microcosm#776: years_since_us_entry from PEINUSYR) up to main. Code conflicts keep both sides: main's #719 work-experience inputs and #776's entry clock in the release input coverage register and manifest tool; main's waiver-free catalog with #776's extra column (178 keys). Count pins add #776's one late target to main's (73 late, 54 numeric, 137 battery targets, 121 transfer targets, 30 ASEC source producers). Where main and #776 each moved a count from the same base, git merged the edit once; those (producer/catalog closure 136, pool provisioning 761/124) were corrected from the generators' observed values, as were main's stale 71/135 docstrings. #1484 retired the catalog unit waiver, so the clock is reviewed into _REVIEWED_UNIT_BY_INPUT as a duration ("year" -> "years"), and "years" joins the catalog unit enum. #1052 consolidation: the ACS local immigration stage reuses the immigration stage's column, owned-column tuple, engine default, bar and foreign-born codes instead of redefining them, and documents that it keeps a donor's stored clock. Its whole-file gate now also grades the stored clock, so one test expects the file-level clock failure as well. Generated artifacts take main's version and are regenerated: repin_us_spec_engine_contracts --legacy-only, generate_us_bundle_from_constants, repin_us_pool_engine_contracts, repin_us_spec_engine_contracts, spec_engine_coverage, repin again, build_us_release_input_coverage_manifest; every --check passes. US bundle spec_sha256 c46f0ea7. The repin also carries #1496's seed-protocol/seed-map correction. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Observed with the merged tree; each moves only because of #776's clock: - test_us_multispine_pool: transfer-target digest dd0b1be5; the pool input surface is 144 (main's #1239 and #776 each took it from 142 to 143, which git merged once); provisioning 761 declared-absent / 124 materialized, as repin_us_pool_engine_contracts derived. - test_us_acs_transfer: the three ACS transfer execution-contract digests main added after #1085's base. The contract carries the discrete numeric targets; with years_since_us_entry removed they reproduce main's digests exactly. - test_spec_engine_stacked_authority_semantics: the authority receipt (b9ce079f), its declared-surface, late-schedule, metric-registry and post-PUF-transfer components, and the late schedule (3b9801f8), read from the live stacked_spine_authority_receipt. - test_spec_engine_typed_closure: main's producer-graph pins gain #776's authored output (93) and compiled outputs (232), matching the inventory counts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
#1496 re-pins the US seed-protocol and seed-map digests, the coverage receipt and the workflow spec_sha256 after #1052 and #1058. This branch already regenerated those pins with the same tools on top of #776's changes, so the three conflicted files keep this branch's values: inventory_coverage.py, docs/evidence/spec-engine/us-f0-coverage.json and engine_workflow/us/test_us_multispine_pool_tool.py. repin_us_spec_engine_contracts --check and spec_engine_coverage --check pass (42267/42267 fields, 41/41 inventory checks). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he clock's coverage key Found by the Modal engine-free and engine-us runs on the merged tree: - test_spec_engine_engine_abi pins the remaining-stage manifest digest that repin_us_pool_engine_contracts moved (d4fb9de9 -> 7ae93314; still 1,059 rows). tools/graph_us_post_transfer_fixture.py regenerated the H3 parity fixture; only that digest changes in us_post_transfer.json (expected.csv is unchanged). - test_spec_engine_plan_lock (added on main after #1085's base) counts the seed stream map's sites: 53 -> 54 with immigration_arrival_year_draw. - test_spec_only_country_packages (tightened on main by #1440) reads every dict key as a possible Python entrypoint and refuses the token "entry". The release input coverage manifest keys its columns by PolicyEngine-US variable name, and years_since_us_entry is the first such name with that token. A plain snake_case key directly under that manifest's "columns" is now admitted; its row is still scanned. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_legacy_entrypoint_publication_matches_origin_main_golden observed on Modal (engine-free, managed CPython 3.13.12, no inherited PYTHONPATH): the pool H5 and agreement goldens are unchanged, and the manifest moves to ba39a694 because #776's clock re-derives the pool engine contracts and late registry identities the checkpoint identity binds. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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 #776. (#1077 was closed as a duplicate of #776.)
Refs #1020, #1052, PolicyEngine/policyengine-us#9732
The blocker is gone: PolicyEngine/policyengine-us#9737, which models the ICHIA/CHIPRA §214 lawfully-residing option, merged on October 5, 2026. This branch is now merged up to
mainat79a533c7(November 3, 2026), which includes #1052's ACS local immigration stage and #1496's spec-engine repin.Problem
The default US build stores no
years_since_us_entry, so every person gets the policyengine-us default of 5: past the federal five-year bar (8 USC 1613). PolicyEngine/policyengine-us#9732 removes that default, which would set the clock to 0, and is held until the data supplies the column. Without a stored clock, the bar, the Missouri SSP waiting period and the Washington TANF and RCA windows all run on a default instead of the survey. The CPS ASEC already carries the raw signal inPEINUSYR(year of entry to stay).Design
The immigration stage (
us_runtime/immigration.py) now owns a third person column,years_since_us_entry(float). The two status labels are unchanged.PRCITSHP4/5): the time period minus an arrival year, clipped at 0.PRCITSHP1-3, which includes people born in Puerto Rico or the territories and those born abroad to a US parent): age (A_AGE). This is Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052's convention; citizens short-circuit every engine consumer.with_us_immigration_inputsunchanged; the engine default still applies to it. A clock stored without the labels is refused as a partial surface.Band mapping (verified against the Census data dictionaries)
Each code was checked in the
PEINUSYRvalue lists ofcpsmar23.pdftocpsmar26.pdf(https://www2.census.gov/programs-surveys/cps/techdocs/):Census re-bins the top codes every year, so a code is read in the codebook of the person's own vintage. The survey year is
source_year + 1. A frame withoutsource_yearis read as one file of the period's income year.The stage refuses rather than guesses in three cases:
Straddle rule
Every consumer compares the clock with a whole number of years:
>= 5for the bar and the MO SSP wait, and< windowfor the ORR window. A band midpoint would put a straddling band wholly on one side, so the arrival year is drawn uniformly over the band's calendar years, using the stage's seeded blake2b keyed draw:immigration:arrival_year, so the status labels are untouched.source_year:source_person_id, so support clones share one entry year.immigration_arrival_year_draw.Two bounds apply before the draw:
survey year - A_AGE - 1. That also bounds the open "Before 1950" band.With the 2024 period, no two-year band straddles five: 2018-2019 gives 5-6 years and 2020-2021 gives 3-4. In an odd period every two-year band does, and the wide top codes straddle the one-year window, so the draw keeps the rule exact in expectation for any period.
National pool and ACS rows
The column joins the
immigrationoperator output family (operator_boundary), so the multispine pool'ssource_operator_immigrationtransfer family becomes(immigration_status_str, ssn_card_type, years_since_us_entry). On ACS rows the clock is a chained-QRF target fitted after the joint status pair, so it is conditioned on the transferred status. It is registered as a discrete numeric target, which keeps transferred values on the donors' whole-year support. #1052 / #1020 remain the route to exactYOEPyears on ACS rows.Catalog unit
Since #1484 every numeric pool input needs a catalog unit. policyengine-us 2.2.1 declares
unit = "year"onyears_since_us_entry, and "year" also marks calendar years (birth_year), so the generator refuses to map it on its own. The clock is reviewed into_REVIEWED_UNIT_BY_INPUTas a duration,("year", "years"), andyearsjoinshoursandweeksin the unit enum ofspec_engine/schema/catalogs.schema.json.Relationship with #1052 (merged November 3)
#1052 added the ACS local lane's immigration stage (
us_runtime/acs_local_immigration.py). It fills only missing cells: exactYOEPclocks on ACS rows, and, on donor rows without a clock, thePEINUSYRbin year ofimmigration._asec_arrival_year. The two now fit together this way:YEARS_SINCE_US_ENTRY_COLUMNisUS_IMMIGRATION_YEARS_SINCE_ENTRY_COLUMN,ACS_LOCAL_IMMIGRATION_COLUMNSisUS_IMMIGRATION_OWNED_PERSON_COLUMNS, andENGINE_DEFAULT_YEARS_SINCE_US_ENTRY,FIVE_YEAR_BAR_YEARSand the foreign-bornPRCITSHPcodes come from the immigration stage. The public names Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052's tests and tools import are unchanged.YOEPclock still fills them. Only the national pool's ACS rows get the clock by transfer.acs_local_immigration_signal_gaterunsus_immigration_composition_gateon the whole file, and that gate now also grades a stored clock. A missing clock cell is therefore named for its spine and once more for the file; the gate fails in the same cases as before. One Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052 test expectation and the gate docstring say so.NEVER_DEFAULT_FILLEDentry covers the ACS local lane; the national lane has no default-fill register (its ACS rows are filled by transfer).The arrival-clock caveat María raised on #1052 applies here too: neither the ASEC nor the ACS records when qualified status was granted, so the clock runs from physical entry. That is modeling decision 2 below. #1052's
adjustment_lag_sensitivityreceipt sizes the affected group for the ACS local lane; the national lane has no equivalent receipt yet.Registries and pins
Values are main (
31c89390) → this branch.source_stages.jsontest_us_bundle_core_contracts,test_support/us_spec_bundle):1683f9cd…→d11d99fd…tools/generate_us_bundle_from_constants.py):sources.yaml,imputation.yaml,catalogs.yaml,battery.yaml,spine.yaml,engine_abi.lock.json(policyengine-us still 2.2.1); spec_sha256fe3901b5…→c46f0ea7…yearsmonetary_sign_separated81 → 82); surface counts 136 → 137, 72 → 73 late, 29 → 30 ASEC-source, 120 → 121 transfer; completion receipt 72 → 73tools/repin_us_pool_engine_contracts.py: provisioning 762/123 → 761/124 (declared-absent / materialized), remaining-stage manifestd4fb9de9…→7ae93314…(still 1,059 rows; also in the engine ABI lock test and the H3 parity fixture, regenerated withtools/graph_us_post_transfer_fixture.py)docs/evidence/spec-engine/us-f0-coverage.jsonregenerated99fafd88…→4db18a3a…, seed protocol61b4206a…→89789314…, …) and the count vector (alsotest_support/spec_engine_inventory_coverage.py)immigration_arrival_year_draw(ownerimmigration_status); 53 → 54 sites, 112 → 113 bindingsrelease_input_coverage.POST_REFERENCE_ECPS_REQUIRED_INPUTSand the manifest tool; manifest regenerated: 171 → 172 required, 6 reviewed exclusions, 177 → 178 columns; required, NO reviewed exclusion (the #368/#462/#978 pattern; today's artifacts are red until a national build carries it)acs_transfer_DISCRETE_NUMERIC_TARGETSus_runtimeexportsf1fb754e→3b9801f8, payload76a79c9d→3f37bc91, authoritybf681aed→b9ce079fand four of its components, the pool transfer-target digest8b234f9a→dd0b1be5, the three ACS transfer execution-contract digests, the pool-tool manifest goldenbfaa7ffc→ba39a694, the plan lock's 54 seed sites, the loader golden vector5892f4b1→f5950b7e)test_spec_only_country_packagesentry;years_since_us_entryis the first release-coverage column name with it, so a plain snake_case key directly underrelease_input_coverage_manifest.json'scolumnsis admitted, and its row is still scanneddocs/us-multispine-operator-ordering.mdWhere main and this PR each moved a count from the same base value (the pool input surface 142 → 143, the producer/catalog closure 134 → 135, pool provisioning 763/122 → 762/123), git merged the edit once; those were corrected to the observed values (144, 136, 761/124).
Summary statistic and gate
us_immigration_composition_summarynow reports the weighted entry-clock distribution:The weighted LPR share under five years is
by_status.LEGAL_PERMANENT_RESIDENT.under_5_years_share. It flows into the base builder's summary JSON and the fiscal-refresh gate details.us_immigration_composition_gatefails a stored clock that is incomplete, negative, or the engine default for every non-citizen. A frame without the clock still passes, as before.Needs confirmation from code owners
Two changes outside the immigration stage need confirmation from whoever owns that code:
entry, soyears_since_us_entryfailed it. I added a narrow exemption for column names listed in the release-input-coverage manifest. Please check that this is the intended scope.yearsnow joinshoursandweeksinspec_engine/schema/catalogs.schema.json, and the clock is reviewed in as a duration (see "Catalog unit" above).Tests
After merging main, every file below was run under WSL one at a time against policyengine-us 2.2.1. Engine-contract files import the engine; the rest are engine-free.
engine_free/us/test_us_immigrationengine_free/us/test_us_acs_local_immigration(#1052)engine_free/us/test_us_acs_local_release_tool(#1052)engine_free/us/test_us_multispine_pool/test_us_late_producer_dagengine_free/us/test_us_acs_transfer/test_us_spine_blindnessengine_free/us/test_us_spec_bundle/test_us_bundle_core_contracts/test_us_catalog_unitsengine_free/us/test_us_spine_agreement/test_us_l0_refit_export/test_us_stored_input_registerengine_free/us/test_us_puf_support_base_builder/test_us_fiscal_refresh_builderengine_free/shared/test_spec_engine_*(loader, seeds, battery, compiler_ir, field_usage, imputation, legacy_adapter, identity_contracts, calibration, inventory_coverage, plan_lock, engine_abi)engine_free/shared/test_spec_only_country_packages/test_release_input_coverageengine_contract/us: coverage_tool, inventory_coverage, release_input_coverage, stacked_authority_semantics, imputation_lineage_spec, us_bundle_core_contracts, typed_closure, us_spec_bundle, engine_abi, us_graph, take_up_semanticsmicrocosm-graphengine_workflow/us/test_acceptance_h_parityruff check .is clean,ruff formatis clean on every touched file, andtools/ci_test_plan.py verifyis ok.--checkis clean forrepin_us_spec_engine_contracts,generate_us_bundle_from_constants,spec_engine_coverage(42,267/42,267 fields, 41/41 inventory checks) andrepin_us_pool_engine_contracts.The full engine-free and engine-us jobs, and the engine-workflow pool-tool and H3 parity files, ran on Modal against
mainat31c89390with the same runner:31c89390530083adThe three engine-free failures are the same on main: a root-user permission test, a worker-recycling test that needs Python 3.13.15 (the runner has 3.13.12), and a transport-evidence module count. The Modal runner drops the container's inherited
PYTHONPATH; with it, about 600 tests error on a duplicatedidnadistribution on main as well.New tests in
test_us_immigration.py:TestEntryClockBands: every fixed band in every vintage; each vintage's top codes; clip at zero; birth-year bounds; age contradictions; the frame withoutsource_year; refusals.TestEntryClockStraddleRule: a straddling band lands on both sides at about 50/50; no straddle across five in 2024; the one-year window; seeded reproducibility; clone consistency.TestEntryClockUsBorn: PRCITSHP 1-3 get their age.TestEntryClockStageOutput: the ASEC labels and every other column are byte-identical to the unchanged label kernels, and the clock is appended last as float64.TestEntryClockFrameIntegrationandTestEntryClockSummaryAndGate.Not yet run on real data. No build was run.
Modeling decisions (for review)
time_period - arrival(as US: store years_since_us_entry from PEINUSYR (blocked on engine ICHIA option) #776 and Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052 specify), and the pooled vintages are not restated. With a 2024 period and the 2024-2026 ASEC pool, the 2025 and 2026 vintages count arrivals after 2024 as 0 years, and their 2020-2021 arrivals as 3-4 years rather than the 4-6 years they had at interview. That raises the pooled share under the bar relative to measuring at interview. For scale, the ASEC 2026 file has 1,047 persons in code 27 against 2,862 in codes 28-29 (all persons, unweighted;experiments/us-asec-2025-source/receipts/measurements_2025.json). The alternative,survey year - arrival, keeps each vintage's own duration distribution; it is a one-line change if reviewers prefer it.years_since_us_entry < window_years), it runs the other way and can wrongly exclude a recently qualified person. This is the caveat María raised on Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052, and it applies here unchanged. Modelling an adjustment lag (from the DHS Yearbook) is deferred._ARRIVAL_YEAR_MIDPOINTS) still drives the DACA and Cuban/Haitian tests, so the labels stay byte-identical. The clock never feeds a label.--base-h5lanes, so it is not done. The release-input-coverage gate makes the missing column visible.YOEP. Assign immigration status and years since entry on ACS rows in the ACS local lane (#1020) #1052 owns that route for the ACS local lane; the national pool's ACS loader does not keepYOEPyet (ACS local lane: immigration_status_str and ssn_card_type default-filled CITIZEN for all ACS persons; years_since_us_entry never populated #1020).Before merge / on the next rebuild
under_5_years_share), against DHS LPR-flow evidence; Derive years_since_us_entry from CPS PEINUSYR (and ACS YOEP) so policyengine-us can drop its default #1077 estimated about 26% of LPRs arrived in 2020 or later;PEINUSYRrefusals (foreign-born code 0, or an unreviewed vintage);🤖 Generated with Claude Code