Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/us-w2-items-tax-year-2020.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Stamp the IRS SOI Form W-2 item facts with the tax year IRS published them for. The pinned US Chronicle feed carried five cells of Table 4.B for Tax Year 2020 (`20in04w2all.xlsx`) with period 2023 and vintage `tax_year_2023`: the Box 7 tips amount, return count and taxpayer count, the Box 12 D 401(k) elective deferrals and the Box 12 AA designated Roth contributions. Latest-vintage selection took the ty2023 tips amount, so the 2024 target aged one year on the CBO wages ratio ($28,280,884,269) where the TY2020 cell needs the SOI wage chain from 2020 ($34,287,530,779, +21.2%). The feed is re-pinned to Chronicle `f98acf4` (`c5e5bf8` plus the W-2 package's literal TY2020 labels, tag `microcosm-us-feed-w2-ty2020-v1`): 39,155 rows, `a81cbcc5…`, scope 585 pairs. No other target changes name, value or metadata; the registry stays at 32,842 targets and `national_state` at 5,694 (`d315c75804ef` to `61a081ae55bb`). The #451 tips return-count exclusion is keyed to the ty2020 id and still holds at every vintage (decision d179). The compile and the exclusion receipt now refuse a W-2 item fact whose period or `tax_year` vintage is not the one tax year its source table's title names. `tools/route_a/route_a.sh` restates the new feed digest, and a test fails if it falls behind the pin. Tax Year 2020 is the latest Table 4.B IRS has published, so there is no TY2023 table to add.
12 changes: 8 additions & 4 deletions docs/us-acs-local-soi-target-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,14 @@ surface's specs.
## The surfaces on the pinned feed

Measured on 2026-09-22 by calling `state_admin_specs(feed, ["snap",
"medicaid", "soi"], soi_mode=...)` on the pinned
`consumer_facts_us_c5e5bf8.jsonl` (sha256 `b8543739…`, the `facts_sha256` in
`packages/microcosm-build/src/microcosm/build/us/chronicle_feed.json`). The
Chronicle `b571381` consumer artifact gives the same counts.
"medicaid", "soi"], soi_mode=...)` on
`consumer_facts_us_c5e5bf8.jsonl` (sha256 `b8543739…`, the feed
`packages/microcosm-build/src/microcosm/build/us/chronicle_feed.json` pinned
then). The Chronicle `b571381` consumer artifact gives the same counts, and so
does the feed pinned since 10 October 2026 (`consumer_facts_us_f98acf4.jsonl`,
which differs only in its Form W-2 item rows):
`test_pinned_feed_soi_surfaces_match_their_contracts` and
`test_pinned_feed_state_cd_surface_matches_its_contract` pass on it.

| Family | `state` | `totals` | `full` |
|---|---:|---:|---:|
Expand Down
158 changes: 146 additions & 12 deletions docs/us-chronicle-feed-repin.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ any other feed, and `test_us_chronicle_feed.py` fails if the parity resources,
the generator and the pin disagree, or if the scope file changes without a
new pin.

The pin has moved twice. September 2026 replaced an unlabelled feed with the
first labelled export (the next section onward). October 2026 moved only the
Form W-2 item rows, which the September feed stamped with the wrong tax year
("The W-2 item tables carry Tax Year 2020" below).

## Why the feed moved

`_validate_chronicle_hierarchy_labels`
Expand All @@ -28,25 +33,32 @@ the first labelled US export.

| Field | Value |
|---|---|
| Chronicle commit | `c5e5bf8aa84960c1a200ee47303b19c953092d0f` |
| Feed file | `consumer_facts_us_c5e5bf8.jsonl`, 39,158 rows, 164,603,204 bytes |
| `facts_sha256` | `b85437390021777e746f507c5890305496baf5fc7f2c78ba08ddb090f4839801` |
| Chronicle commit | `f98acf4edcc9488343446fda46db77914de8611f` (tag `microcosm-us-feed-w2-ty2020-v1`): `c5e5bf8` plus one package file |
| Feed file | `consumer_facts_us_f98acf4.jsonl`, 39,155 rows, 164,591,409 bytes |
| `facts_sha256` | `a81cbcc504caa71e4eb23b4d70a11528c8e6db6523e41283f8482569a23df04c` |
| Consumer fact schema | `chronicle.consumer_fact.v3`, schema file sha256 `bdb51e2a…` (unchanged from the UK pin) |
| Scope | 586 (record set, period) pairs; 62 package runs over build years 2020 to 2029 |
| Scope | 585 (record set, period) pairs; 61 package runs over build years 2020 to 2029 |
| Consumer artifact | none: refused at this commit, see below |

The September pin was Chronicle `c5e5bf8aa84960c1a200ee47303b19c953092d0f`:
`consumer_facts_us_c5e5bf8.jsonl`, 39,158 rows, 164,603,204 bytes,
`facts_sha256` `b8543739…f4839801`, 586 pairs from 62 package runs. The
compile now refuses that feed (`_check_w2_item_fact_tax_years`).

The feed is too large for the repository. Its home on the build machine is
`~/PolicyEngine/_buildh-runtime/inputs/consumer_facts_us_c5e5bf8.jsonl`,
`~/PolicyEngine/_buildh-runtime/inputs/consumer_facts_us_f98acf4.jsonl`,
beside the previous pins; `tools/build_us_target_parity_manifest.py` reads it
there by default. A holder of the Chronicle commit regenerates it byte for
byte with the commands below.

## Scope rule

The feed keeps exactly the 586 (record set, period) pairs of the previous
pin, so the family surface the release parity gate reviews does not move in
this change: 32 compiled families and 52 reviewed exclusions before and after,
the same 81 feed families. Widening the feed to Chronicle's newer US packages
The September feed kept exactly the 586 (record set, period) pairs of the
previous pin, so the family surface the release parity gate reviews did not
move: 32 compiled families and 52 reviewed exclusions before and after,
the same 81 feed families. The October change swaps the three W-2 item pairs
stamped ty2023 for their TY2020 record sets, one of which the scope already
had, leaving 585 pairs and the same families. Widening the feed to Chronicle's newer US packages
is a separate, deliberate change with its own compile-or-fence review per
family. Scoping by record set alone is wrong: it pulls extra Medicaid months
into the snapshot families.
Expand All @@ -72,9 +84,11 @@ minutes where a targeted package run takes one or two seconds.
uv run python tools/build_us_chronicle_feed.py --chronicle-root ~/PolicyEngine/chronicle --out /tmp/us-feed --replace --skip-artifact
```

The checkout must be clean at the scope's commit. The tool runs one
The checkout must be clean at the scope's commit
(`git checkout microcosm-us-feed-w2-ty2020-v1`; the commit is not on
Chronicle main). The tool runs one
`chronicle build-bundle --year Y --source <package> …` per build year (ten
runs, 62 packages), reads each package's own `consumer_facts.jsonl`, keeps a
runs, 61 packages), reads each package's own `consumer_facts.jsonl`, keeps a
row only from the run its pair is scoped to, refuses a pair with no row or
two rows with one `aggregate_fact_key` and different bytes, sorts by
`aggregate_fact_key`, and writes `consumer_facts.jsonl` plus `receipt.json`
Expand Down Expand Up @@ -181,11 +195,131 @@ and target id, and the register entry carries a reviewed `state_name`
(`_geography_fallback_label` is UK-only, so the label cannot be derived from
the FIPS code).

## The W-2 item tables carry Tax Year 2020

10 October 2026. The September feed carried five facts with period 2023 and
vintage `tax_year_2023` whose `source.source_file` is `20in04w2all.xlsx` and
whose `source.source_table` is "Table 4.B. Summary of Items for Taxpayers with
Form W-2, by Return and Earner Type, Tax Year 2020":

| `source_record_id` (as stamped) | Value | Workbook cell |
|---|---:|---|
| `irs_soi.ty2023.form_w2_social_security_tips.box_7_social_security_tips.amount` | 26,786,522,000 | D13 |
| `…form_w2_social_security_tips.box_7_social_security_tips.return_count` | 6,038,613 | B13 |
| `…form_w2_social_security_tips.box_7_social_security_tips.taxpayer_count` | 6,105,713 | C13 |
| `irs_soi.ty2023.form_w2_401k_elective_deferrals.box_12_d_401k_elective_deferrals.amount` | 277,859,181,000 | D22 |
| `irs_soi.ty2023.form_w2_designated_roth_401k_contributions.box_12_aa_designated_roth_401k_contributions.amount` | 32,302,509,000 | D41 |

The three tips rows repeated the feed's correctly stamped `irs_soi.ty2020`
tips facts cell for cell.

**The source.** Checked on 10 October 2026:

- IRS's Form W-2 statistics page
(`irs.gov/statistics/soi-tax-stats-individual-information-return-form-w2-statistics`,
last reviewed 27 January 2026) links Table 4 for 2020 and 2019 only.
`21in04w2all.xlsx` to `24in04w2all.xlsx` return 404. There is no Table 4.B
for Tax Year 2021, 2022 or 2023. The prior-releases page carries tax years
2008 to 2018 in one workbook (`18inallw2.xls`), so 2020 is the latest year
IRS has published.
- A fresh download of `20in04w2all.xlsx` has the sha256 Chronicle pins
(`1178d776…cfc442f`). Cell A1 of sheet "Table 4.B" is the title above, and
rows 13, 22 and 41 hold the five values (money amounts in thousands).

So every one of the five values is a Tax Year 2020 value.

**Why the feed stamped them 2023.** At Chronicle `c5e5bf8` the
`soi-w2-statistics-2020` package sets `artifact_year: 2020`, which makes every
build read that one workbook, but rendered its period, record ids, vintage and
legal vintage from `--year`. The scope built it at `--year 2023` for three
record sets, as the July feed's pairs said to. chronicle#292 (merged 28
September) made the labels literal 2020 on Chronicle main.

**The pin.** Chronicle `f98acf4` is `c5e5bf8` plus only that package file as
#292 left it (branch `feed/us-c5e5bf8-w2-2020`, tag
`microcosm-us-feed-w2-ty2020-v1`). The scope drops the three ty2023 W-2 pairs
and adds the TY2020 401(k) and Roth record sets; it already had the TY2020
tips record set. Against the September feed
(`experiments/us-w2-table-year-repin/`):

- 39,153 rows are byte-identical, the three `irs_soi.ty2020` tips rows among
them;
- the five rows above are gone, and no row carries their ids (a deliberate
supersession: the ids named a tax year IRS has not published);
- two rows are new, the 401(k) and Roth amounts under their `irs_soi.ty2020`
ids, each with the value and the source cells of its ty2023 twin. The
three ty2023 tips rows need no replacement: their TY2020 twins were
already in the feed.

`tools/build_us_chronicle_feed.py` built the feed from the new scope, and an
independent splice (the September feed without the W-2 package's rows, plus
that package's rows at `f98acf4`, sorted by `aggregate_fact_key`) gives the
same bytes.

A pin on Chronicle main is a larger change. At main (`3dce46a6`) seven more of
the September scope's pairs are no longer declared: the congressional-district
file, the two IRA tables and the four `state_2022` US rows, which #292 gave
literal 2022 labels. Moving those changes the year every district dollar
target ages from; microcosm#1030 measures that and lists the decisions it
needs.

**What moved.** Compiled as the release does (target period 2024, district
crosswalk, aging on, Medicaid substitutions), before on main `7639ef8b0` with
the September feed and after on this change:

| | Before | After |
|---|---|---|
| Compiled targets | 32,842 | 32,842 |
| `national_state` targets | 5,694 | 5,694 |
| `national_state` registry | `d315c75804ef` | `61a081ae55bb` |
| W-2 Box 7 tips amount target | $28,280,884,269 | $34,287,530,779 |
| Its name | `irs_soi.ty2023.form_w2_social_security_tips.…amount` | `irs_soi.ty2020.form_w2_social_security_tips.…amount` |
| Its aging factor | 1.05578784246075 (CBO wages 2024 / 2023) | 1.28002921689246 (SOI Table 1.4 wages 2023 / 2020, then CBO wages 2024 / 2023) |

The tips target rises $6,006,646,510 (21.2%), the SOI wage growth from 2020 to
2023 that the ty2023 stamp skipped. It is the only target that changes: the
other 32,841 keep their name, value and every metadata field. Latest-vintage
selection had picked the ty2023 row over its TY2020 twin, so the target aged
one year where `target_aging.py` chains the wages series from 2020. The
September re-pin introduced this: it added the ty2023 tips amount and return
count (the row count of `irs_soi.form_w2_social_security_tips` went 4 to 6,
above; it is now 3). The July feed already carried the ty2023 401(k), Roth and
tips taxpayer-count rows, none of which compiles to a target. The Route A release `populace-us-2024-0fb05b6-4b57d15a287c-20260930T150401Z`
recorded the $28,280,884,269 target
(`tests/fixtures/us_route_a_target_loss_weights.json`).

The 401(k) and Roth amounts stay reviewed exclusions (`input_side`), so they
move no target. Anything that anchors on them later (microcosm#1191 names the
401(k) amount) is anchoring on a Tax Year 2020 level.

**The exclusion register.** The #451 tips return-count entry in
`US_FISCAL_TARGET_SUPPORT_EXCLUSIONS` and
`US_FISCAL_TARGET_ALL_VINTAGE_SUPPORT_EXCLUSIONS` is keyed to the ty2020 id.
Decision d179 is unchanged: the cell is excluded at every vintage. The receipt
lists one tips id where it listed two.

**The check.** `_check_w2_item_fact_tax_years` runs at the start of
`compile_us_fiscal_target_registry` and of
`us_fiscal_target_exclusion_receipt`. For a fact of the three W-2 item
families (`irs_soi.form_w2_item` layout) it requires the source table's title
to name exactly one tax year, and that year to be the year target aging reads
from the fact's period and the year in its `tax_year_<year>` vintage. It
refuses the September feed and names the five rows.
`test_us_w2_item_tax_year.py` holds the tests. Chronicle's
`tests/test_chronicle_source_table_year.py` checks the same thing at the
source for every pinned package whose title names one tax year.

The parity resources were regenerated: 32 compiled families, 52 reviewed
exclusions and 81 feed families, as before. `tools/route_a/route_a.sh` restates
the new `FEED_SHA`; `test_us_chronicle_feed.py` now fails if it falls behind
the pin.

## The consumer artifact is refused at this commit

`chronicle build-consumer-artifact` validates every row against
`consumer_fact.v3`, whose `concept_alignment` object has required `authority`
since Chronicle `cafc583`. `build-bundle` at `c5e5bf8` emits 994 rows whose
since Chronicle `cafc583`. `build-bundle` at `c5e5bf8`, and at `f98acf4`, which
differs from it in one W-2 package file, emits 994 rows whose
`concept_alignment` has `relation: source_label` and no `authority`, in three
packages: `cms-medicaid-chip-monthly-enrollment-dataset` (515),
`census-b01001-female-age-2023` (468) and `jct-tax-expenditures-2024` (11).
Expand Down
8 changes: 8 additions & 0 deletions docs/us-fact-to-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ the release PROGRESS notes). Re-pin the sha everywhere it is enforced: the
launch scripts' `FACTS_SHA` guards, `--ledger-facts-sha256`, and the PROGRESS
doc. The builder refuses to start on a mismatch.

An id the package no longer emits leaves the feed without a replacement. The
October 2026 re-pin dropped five `irs_soi.ty2023` Form W-2 item ids that way:
they named a tax year IRS has not published for that table
(`docs/us-chronicle-feed-repin.md`). Check a new fact's period against the
year in its source table's title before it ships. For the W-2 item tables the
compile enforces it: `_check_w2_item_fact_tax_years` refuses a fact whose
period or `tax_year` vintage is not the one tax year its title names.

## 2. Run the support oracle BEFORE wiring anything

A target with no model support cannot bind; a target with thin support binds
Expand Down
58 changes: 58 additions & 0 deletions experiments/us-w2-table-year-repin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# W-2 item tables: re-pin the US feed to the tax year IRS published

10 October 2026. The pinned US Chronicle feed stamped five cells of IRS SOI
Table 4.B for Tax Year 2020 as ty2023. `docs/us-chronicle-feed-repin.md`
("The W-2 item tables carry Tax Year 2020") has the account; this folder holds
the scripts and their outputs.

| File | What it does |
|---|---|
| `relabel_scope.py` | Rewrites `us/chronicle_feed_scope.json`: the three ty2023 W-2 pairs out, the TY2020 401(k) and Roth record sets in, the new Chronicle commit. Idempotent. |
| `splice_feed.py` | Predicts the new feed from the old one and one run of the W-2 package (`docs/us-fact-to-target.md`, step 1). |
| `apply_pin.py` | Writes the rebuilt feed's digests into `us/chronicle_feed.json` and the parity generator's feed name, from the builder's receipt. |
| `compare_feeds.py` | Row-by-row difference between two feeds. Output: `feed_compare.json`. |
| `measure_surface.py` | Compiles a feed as the release does and lists every target. |
| `compare_surfaces.py` | Target-by-target difference between two `measure_surface.py` reports. Output: `compare.json`. |
| `receipt.json` | The receipt `tools/build_us_chronicle_feed.py` wrote for the pinned feed. |

## Order of work

```bash
python experiments/us-w2-table-year-repin/relabel_scope.py packages/microcosm-build/src/microcosm/build/us/chronicle_feed_scope.json f98acf4edcc9488343446fda46db77914de8611f
```

```bash
uv run python tools/build_us_chronicle_feed.py --chronicle-root <chronicle checkout at microcosm-us-feed-w2-ty2020-v1> --out <feed dir> --replace --skip-artifact
```

```bash
python experiments/us-w2-table-year-repin/apply_pin.py . <feed dir> f98acf4edcc9488343446fda46db77914de8611f consumer_facts_us_f98acf4.jsonl
```

Then copy `<feed dir>/consumer_facts.jsonl` to
`~/PolicyEngine/_buildh-runtime/inputs/consumer_facts_us_f98acf4.jsonl` and run
`uv run python tools/build_us_target_parity_manifest.py`.

## Results

`feed_compare.json`: of the old feed's 39,158 rows, 39,153 are byte-identical
in the new feed; the five ty2023 W-2 ids are gone; two ids are new (the
401(k) and Roth amounts at ty2020), each with the value and source cells of
its ty2023 twin. The new feed has 39,155 rows.

`compare.json`: "before" is main `7639ef8b0` compiling the old feed, "after"
is this change compiling the new one. Both compile 32,842 targets, 5,694 of
them on `national_state`. One target differs: the W-2 Box 7 tips amount is
`irs_soi.ty2023.…amount` at $28,280,884,269.41 before and
`irs_soi.ty2020.…amount` at $34,287,530,778.93 after. The other 32,841 match
on value and on a digest of all their metadata.

"Before" has to run on main's code, because this change refuses the old
feed. It ran from `git archive origin/main packages/microcosm-build/src tools`
with that tree first on `PYTHONPATH`; the report's `compiler` field recorded
which `fiscal_targets.py` did the compile.

The splice and the builder agree: `splice_feed.py` on the old feed and one
`chronicle build-bundle --year 2020 --source packages/irs_soi/w2_statistics_2020`
run at `f98acf4` gives the bytes `tools/build_us_chronicle_feed.py` writes
from the new scope (`facts_sha256` `a81cbcc5…23df04c`).
Loading
Loading