Repository navigation
Conversation
policyengine-us declares in_denver, in_yonkers, in_wilmington and pa_philadelphia_wage_tax_resident as inputs with no formula. The registry maps each to its 2020 county (Denver 08031, Philadelphia 42101) or place (Yonkers 36/84000, Wilmington 10/77580), checked against the 2020 Gazetteer, P.L. 94-171 P1_001N and block ladders 7ba39b95 and 6840b990. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The block-ladder frame wrapper derives all four flags from the block's county and place. The PUMA ladder and the pooled ACS line derive the county-rule flags (in_denver, pa_philadelphia_wage_tax_resident) from the final county and remove the place-rule flags (in_yonkers, in_wilmington), which no place rung can decide there. Summaries record each flag as derived or unavailable, and both ladder gates fail a present flag that disagrees with its rule. The pool manifest moves the county-rule flags to geography_derived_jurisdiction_input (assembled) and the place-rule flags to unavailable_place_jurisdiction_input, and relabels county_fips and congressional_district_geoid, which the stacked PUMA operator writes, as preserved_stacked_engine_input. tools/repin_us_pool_engine_contracts.py re-derived the provision counts, the manifest digest and the engine ABI lock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-derived with the repo tools after the derivation change: repin_us_spec_engine_contracts.py (--legacy-only, then full), generate_us_bundle_from_constants.py (US spec_sha256 0b5c29e4...) and spec_engine_coverage.py (42305/42305 fields, 41/41 checks); --check passes on all three and on repin_us_pool_engine_contracts.py. Tests: Hypothesis properties for the iff rule, block-population conservation, row-order invariance and person broadcast, plus registry pin, gate, PUMA and pooled-line cases (engine_free); an engine contract that each flag is an input-only boolean its consumers read; and an engine scenario in which Yonkers, Wilmington, Denver and Philadelphia residents placed by the block ladder get their local tax or benefit and their neighbours do not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, record the no-ladder pool receipt Answers the round-1 review of b7b3e21: - rederive_us_local_jurisdiction_flags reads the rungs an existing artifact's own geography supports (county when county_fips is on every row; place only when place_fips and a 15-digit block_geoid are too), refuses carried household or person flags that disagree, and writes the flags. export_us_l0_refit_h5 and the fiscal refresh release builder call it on the loaded base and record the receipt. - Both ladder gates fail an absent flag whose rule the line's own rung decides (block: county and place; PUMA: county), so a missing column can no longer reach the engine as an all-False default. - build_optional_acs_multispine records the pool's flag receipt in provenance on both the ladder and no-ladder paths. Also classifies local_jurisdictions.py for the spine-blindness test, repins the engine ABI digest, and reruns the US spec repin chain (seed-kernel attestations hash geography_ladder.py and puma_ladder.py). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nifest tools/graph_us_post_transfer_fixture.py rewrote the pinned graph's remaining_stage_manifest_sha256 (d4fb9de9 -> 6e70bc74) after this PR moved the pool manifest. Sources and expected.csv are byte-identical; the tool's own graph-parity check passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as unavailable pa_philadelphia_wage_tax returns 0 unless pa_philadelphia_wage_tax_taxable_wages is nonzero, and no line wrote it. A new SHA-pinned registry, us_local_tax_inputs.json, decides the five related person inputs: - pa_philadelphia_wage_tax_taxable_wages: max(employment_income_before_lsr, 0) for residents (the regulations tax a resident's compensation wherever earned, deferrals and Section 125 reductions included), 0 for everyone else. The nonresident component is unavailable: microcosm reads no ACS place-of-work field, and ACS codes elective home workers to their home. - pa_philadelphia_wage_tax_reduced_rate_eligible: unavailable (refund_claim); the 1.5% rate comes only by petition after the year ends. - ny_yonkers_nonresident_earnings, de_wilmington_nonresident_earnings, co_denver_employee_occupational_privilege_tax_months: unavailable (work_location); the smallest place-of-work units are whole counties. Unavailable inputs are never written, and stale columns are removed. The block ladder, the stacked pool (a derive operator before the QBI reconciliation), the pooled ACS line (propagating PUMS blank wages), re-derivation of existing artifacts and annual static aging all write the taxable wages once residence and wages are final. Repins the pool manifest (derive 34 -> 39, digest 92cad0dc), the spec chain (US spec_sha256 c69a4313, 42347/42347 fields) and the legacy publication golden's manifest identity. Adds Hypothesis properties, gate and refusal cases, a pooled-line propagate test and an engine scenario in which a Philadelphia resident built through the block line owes wages times the resident rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gest tools/graph_us_post_transfer_fixture.py rewrote us_post_transfer.json; only the derive node's remaining_stage_manifest_sha256 changed (6e70bc74, the manifest with the flag change, to 92cad0dc, which adds the local-tax derive operator). expected.csv is unchanged and the tool's graph-parity check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The derive entrypoint's operators mapping listed with_us_local_tax_inputs after the QBI reconciliation. It now lists the kernels in POOL_DERIVE_OPERATOR_ORDER, as the structure check requires, and the check knows the receipt wrapper. The L0 refit-export and derive-stage fixtures gain employment_income_before_lsr, which every built frame carries. Two tests show a Philadelphia resident's wages becoming the taxable wages, one through the L0 refit export of a base that predates the input and one through the pool derive stage, where no later operator rewrites the wages the base read and a resident's negative wage gives 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
us-local-work-location-inputs
branch
from
November 2, 2026 17:54
a805259 to
5a7135f
Compare
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.
Stacked on #1453 (
us-local-jurisdiction-flags, head51022c0de). The base is main so CI tests the combined state; until #1453 merges, the diff also shows its commits. This PR's own change is51022c0de..HEAD. Merge #1453 first.Problem
#1453 derives
pa_philadelphia_wage_tax_residentfrom household geography, but the flag alone moves no tax. In policyengine-us 2.2.1,pa_philadelphia_wage_taxsumspa_philadelphia_wage_tax_taxable_wagestimes the rate and returns 0 when no person in the tax unit has taxable wages. No microcosm line writes that input, so every built dataset still computes zero Philadelphia wage tax.The engine ABI lock listed five related person inputs as
declared_absent_engine_input. Each has no formula and no uprating in policyengine-us 2.2.1 and 2.60.4:pa_philadelphia_wage_tax_taxable_wagespa_philadelphia_wage_taxpa_philadelphia_wage_tax_reduced_rate_eligiblepa_philadelphia_wage_taxny_yonkers_nonresident_earningsny_yonkers_income_taxde_wilmington_nonresident_earningsde_wilmington_earned_income_taxco_denver_employee_occupational_privilege_tax_monthsco_denver_employee_occupational_privilege_taxDecisions
A new SHA-pinned registry,
us_runtime/us_local_tax_inputs.json(e5592932…), records each decision, its reason and its sources.local_jurisdictions.pyrefuses the registry unless its bytes match the pin.pa_philadelphia_wage_tax_taxable_wages: derived for residents. A resident's value ismax(employment_income_before_lsr, 0); everyone else gets 0.employment_income_before_lsris the engine's gross wage input;irs_employment_incomesubtracts pre-tax contributions, so the gross input is the closer match.The nonresident component: unavailable. A nonresident owes tax on pay for days worked in Philadelphia. ACS PUMS POWPUMA 42/03200 is exactly Philadelphia County, so a place-of-work source exists in principle. But:
Nonresidents therefore hold 0, the engine default, and the registry, receipts and manifest record the component as unavailable. Wiring POWSP/POWPUMA is a separate change (follow-up below).
pa_philadelphia_wage_tax_reduced_rate_eligible: unavailable (refund_claim). Employers withhold the full rate. A worker approved for PA Schedule SP tax forgiveness gets 1.5% only by filing a refund petition after the year ends. The City reports 1,421 refunds for tax year 2020; the City Council estimated up to 60,000 eligible households. No survey records the petition, and eligibility alone would overstate the relief many times over. The input stays unwritten, so the engine applies the standard rates.The three work-location inputs: unavailable (
work_location). No defensible source exists at microcosm's geography:The registry pins the ACS PUMS 2022 and 2023 code lists (the PUMA-to-POWPUMA and county-to-POWPUMA sheets), the 2020 PUMA criteria, the ACS subject definitions and the LODES 8.4 technical documentation by SHA-256.
Change
local_jurisdictions.pygains the registry loader,us_resident_wage_tax_values,apply_us_local_tax_inputs,with_us_local_tax_inputsand the gate checkus_local_tax_input_failures. Every line now writes the input once it has both the residence flag and final wages:tools/build_us_puf_support_base.py, monolithic and staged): right afterwith_household_us_geography_ladder. The assignment summary records the receipt aslocal_tax_inputs, and theblock_ladder_assignmentstage boundary names the new operator.with_us_local_tax_inputs. The PUMA ladder draws residence at assembly, before the wage transfers, so the input belongs in the derive stage. It runs after the Schedule D completion and before the QBI reconciliation, whose live-output check binds the person-column inventory. The derive receipt carries the receipt.with_optional_acs_spine, legacybuild_optional_acs_multispine): after the flags, on every row.null_wages="propagate", a resident's input stays null wherever the wage is null.pa_philadelphia_wage_tax_taxable_wagesis not a declared dataset source input, so the register accepts it.rederive_us_local_jurisdiction_flags, which Derive US local-jurisdiction flags (Denver, Yonkers, Wilmington, Philadelphia) from household geography #1453 calls fromexport_us_l0_refit_h5and the fiscal refresh release builder, now derives the inputs after the flags. It refuses a carried input that disagrees with the artifact's own flags and wages.Every unavailable input stays unwritten, and any stale column is removed. The receipt records each input's status and reason, the resident counts and, given weights, the weighted resident wage base.
Engine ABI lock.
tools/repin_us_pool_engine_contracts.pyre-derived the pool manifest:derived_resident_wage_tax_input(availablederived, fallbacknonresident_work_location_unavailable_engine_default);unavailable_work_location_input;unavailable_refund_claim_input;with_us_local_tax_inputs: the residence flag, the transferred wage,person_household_id,household_idand the household weight. Derive rows go 34 → 39 and total rows 1,059 → 1,064.6e70bc74…→92cad0dc…. The stacked checkpoint identity binds it, so resume refuses a pre-change checkpoint.Spec pins. The chain in
tools/repin_us_spec_engine_contracts.py(legacy-only, bundle, full, coverage, full) re-derived the bundle (US spec_sha256c69a4313…).spec_engine_coverage.pyreports 42,347 of 42,347 configuration fields and 41 of 41 inventory checks.--checkpasses on all three spec tools and on the pool tool.Invariants
These hold for every input; Hypothesis tests in
tests/engine_free/us/test_us_local_tax_inputs.pycheck them.max(wages, 0); every other person's equal 0. So0 <= taxable <= max(wages, 0).us_local_tax_input_failurespasses exactly the column the rule writes, or its absence. It fails any other value, a present unavailable input and, withrequire_derivable, an absent input whose flag is present.The derive-stage contract test checks one more, for the stacked pool: no operator after
with_us_local_tax_inputsrewrites the wages it read, so the written value matches the released wages.Mutation check (Hypothesis DB cleared per mutant), each killed by the new tests:
max(·, 0);require_derivable.Tests
engine_free/us/test_us_local_tax_inputs.py(new):engine_scenario/us/test_us_local_tax_inputs_engine.py(new): households carry onlyemployment_income_before_lsr, are placed on fixture blocks bywith_household_us_geography_ladderand built withwith_us_local_tax_inputs, then run throughPolicyEngineUSEngine.materialize.engine_free/us/test_us_base_pool.py: the pooled line re-derives a stale carried value on every row, and a resident ACS child's blank wage leaves a null input. Seeds 0 and 2 place the ACS household in Philadelphia, the others in Delaware County.engine_free/us/test_us_acs_multispine.py: the pooled receipt records the input as unavailable without a ladder and derived (propagate mode) with one.engine_free/us/test_us_local_jurisdictions.py: the re-derivation round trip covers the taxable wages.engine_free/us/test_us_multispine_pool.py,engine_contract/us/test_us_multispine_pool.py,engine_contract/us/test_spec_engine_engine_abi.py:POOL_DERIVE_OPERATOR_ORDER, and the structure check knows the local-tax receipt wrapper;engine_free/us/test_us_l0_refit_export.py: the fixture frame gainsemployment_income_before_lsr, which every built base carries. A new case exports a base that predates the input with one household moved onto a Philadelphia block: its people's wages become their taxable wages, the receipt marks the input derived with the weighted wage base, and no work-location input appears.engine_free/us/test_us_multispine_pool_tool.py: the legacy publication golden's manifest identity moves with the remaining-stage manifest.pool_h5andagreementstay unchanged.microcosm-graphH3 parity fixture:tools/graph_us_post_transfer_fixture.pyregeneratedus_post_transfer.json. Only the derive node'sremaining_stage_manifest_sha256changed: Derive US local-jurisdiction flags (Denver, Yonkers, Wilmington, Philadelphia) from household geography #1453 regenerated it to its own digest,6e70bc74…, and this PR moves it to92cad0dc….expected.csvis unchanged, and the tool's own graph-parity check passes.Not covered
us_geography_ladder_gate,us_puma_ladder_gate) take only the household table, so they do not check this person input. Each line's derivation raises on anything it cannot decide, and re-derivation and aging run the gate check.employment_incomebut not this input.Follow-ups
Running Route A builds pin their own commit and are untouched.
axiom: n/a: microcosm input provisioning; no country-model rule changes.
🤖 Generated with Claude Code