Extract the NIPA tables the value-added block is built from (#536) - #691
Conversation
Adds 24 tables to BEA_NIPA.yaml, all annual and complete for 2012-2024, at roughly 950 extra FBA rows per year: the controls (1.10, 1.1.5, 1.3.5, 1.14), compensation (6.2D, 6.3D, 6.10D, 6.11D, 3.10.5, 7.18) and gross operating surplus (6.12D-6.22D, 7.5, 7.7, 7.9, 3.8, 7.3.5, 7.4.5, 7.11). T00TOP and T00SUB are deliberately absent. Their commodity-axis counterparts shipped in Step 4d (#690), and their industry split is an output of Step 5's balance rather than an input to it, so sourcing them here would estimate the same money twice on two axes. Three findings, all pinned by the new test rather than left in prose: - Take the *paid* line, not the table's root. 6.2D and 6.3D each state their total twice - line 1 received by residents, line 2 paid by domestic industries and government - and value added wants line 2. On it, wages plus both supplements close to 0 against compensation and land on the Use SUT's V00100 within 3. Reading line 1 is what left the ~10,600 the compensation plan carried as an open item; it is the rest-of-world adjustment A4187C, stated in 6.2D's own lines 97-99. - 6.11D is three panels under one code, and only the first is by industry: lines 1-20 industry, 22-36 type of fund, 37-45 benefits paid - a different concept at 2,370,770. Its industry grain is 17, not the 36 the plan claimed, and selecting the whole table double-counts. T61600D, T71100 and T11400 also restate a code; select by line in all four. - Table 3.8 gives the government-enterprise surplus an industry axis it was thought not to have, federal and state-and-local summing to 1.10's A108RC exactly. The config half of the test needs no data and guards the substring hazard in bea_nipa_parse, which selects a table's series with str.contains: two declared ids where one contains the other would contaminate silently. NIPA_final_dom_uses_2017 rebuilds bit-identical - all 17 columns, same row count - so the added rows reach no existing method. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ache The plan's Phases 1-5 were written as a pure-Python pipeline. With Step 2 settled as three FBS methods, most of it survives as configuration rather than code: BEA_Detail_Use_SUT already melts the Use SUT through VAPRO, so the 2017 benchmark V00100 by industry is an attribution source today, and Phase 1 is a selection_fields clause rather than an extraction. The one step with no FBS primitive is the anchor-and-move itself - multiplication does not preserve the group total and there is no renormalise step, which is what the old Phase 3.4 needed. Recorded as the design seam: Phases 1-2 build a cached FBS_outside_flowsa source holding the moved shares, Phase 3 is a single proportional attribution against it, and the exact rescale then holds by construction instead of being a follow-up step. QCEW is cached locally as per-year FBA parquets because generating it is slow (~9M rows a year, county grain). Dropping one into extract/output_data lets getFlowByActivity find it through the ordinary local path with no code change - esupy matches on name and ignores the v2.0.4 tag. Verified for 2017. Four things measured off that cache rather than assumed: - QCEW national NAICS-6 payroll is 93.8% of NIPA wages paid. That number is what settles "allocator, never control", and the missing 6.2% concentrates in the sectors Phase 4 already carves out. - 1,027 of the 1,048 NAICS-6 codes in the BEA detail crosswalk are present, so the gap is a bounded list of 21 rather than a structural hole. - Ownership is on the flow, not a separate axis. It still does not separate government enterprises from general government, which is the distinction the SUT needs - so government stays on NIPA, and the plan's claim is narrowed to what is actually true. - The cache is 2017-2023, so the nowcast's 2024 has no QCEW and needs a stated fallback. Better found here than in Phase 3. Employment_common.yaml cannot be reused as-is: every _bls_selection_fields_* block hardcodes Class: Employment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QCEW covers 93.8% of NIPA wages paid. The plan treated the other 6.2% as an unexplained residual to spread pro rata. NIPA states it. 7.18 closes it exactly: BLS published 7,968,336 + misreporting 106,273 + not UI-covered 399,801 + timing 0 = 8,474,410, wages received. And the gap is already non-uniform before any industry table is opened - government's uncovered rate is 11.3% of government wages against private's 3.5%. 6.4D is the table that turns the assumption into a measurement: QCEW publishes employment on the same axis, so QCEW/NIPA is a coverage ratio per industry. Measured on 2017 private employment it is 97-100% almost everywhere and collapses in exactly two places - educational services 77.1%, other services except government 63.0%, where religious and grantmaking organisations are largely UI-exempt. So QCEW growth is trustworthy broadly and the exceptions are a named short list, which is a better method than a flat haircut. Construction at 97.1% is a warning rather than a reassurance: the count agrees because both sources count the same workers, and the trade-versus-structure-type disagreement that actually matters is invisible to a coverage ratio. Three lookups located: RfHhInstComp W151RC is 18,684, the SUT's 814000 to the dollar - the sector QCEW covers worst is the one NIPA hands over directly, and the plan had described this line without finding it. U32500 splits general government compensation into wages and supplements, which 3.10.5 only totals. 6.6D wages per FTE is the plausibility check Phase 5 lacked: a share can be positive, sum to one, and still imply an implausible wage. Adding those tables exposed two defects in bea_nipa_parse: - Scale and unit were assumed, not read. A flat `* 1000000` with Class and Unit hardcoded to Money/USD is right only while every declared table is in millions of dollars. 6.4D/6.5D are thousands of persons and 6.6D is a ratio, so 6.4D's 150,654 thousand employees would have been published as 1.5e11 USD. Both now come from each series' own MetricName/DefaultScale, with an explicit map that raises on an unrecognised metric rather than defaulting to dollars. Every dollar table BEA publishes has DefaultScale -6, so all 1,812 dollar rows are bit-identical across the change. 1.14's three chained-dollar lines move to Class Other, so a Class: Money selection cannot add real dollars to nominal. - Table ids were matched by substring. Asking for U70205 also returned U70205S's 44 physical-quantity and price-index series under their own id. Nothing downstream selected them so it never showed, and it was the metric guard above that surfaced it. Matched exactly now. NIPA_final_dom_uses_2017 still rebuilds bit-identical, and 2012/2019/2020/2024 extract cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Update: the QCEW-to-NIPA wage gap, and two parser defects it exposedPushed 7.18 closes the gap exactly
And it is already known to be non-uniform before any industry table is opened: government's uncovered rate is 11.3% against private's 3.5%. 6.4D turns the assumption into a measurementQCEW publishes employment on the same axis, so QCEW/NIPA is a coverage ratio per industry. 2017, private employment, thousands:
Coverage is 97–100% across most of the economy and collapses in exactly two places. So QCEW growth can be trusted broadly and the exceptions are a named short list — a different method from a flat 6.2% haircut. Three lookups located
Two parser defects the new tables exposedScale and unit were assumed, not read. A flat Table ids were matched by substring. Asking for Verification
|
Corrects an overstatement in the previous commit. T00TOP/T00SUB were described as "not sourced here ... building them would estimate the same money twice". That was wrong. They are already built by commodity in Step 4d, so the industry row is a transformation of money that exists, not a second estimate - and the plan's own coordination note said exactly that: "build once, derive the other through the Make/Supply structure". So the open question was never whether to derive it, but whether the derivation works. Now measured for 2017, where the published answer exists. It does not. Using the benchmark market-share matrix from the Supply table, D[c,i] = V[c,i] / T007[c]: T00TOP (TOP + MDTY) correlation 0.202 |error| 114.6% of the row T00TOP (TOP only) correlation 0.211 |error| 109.5% T00SUB correlation 0.676 |error| 79.8% The reason is structural rather than noise. 55.7% of the published T00TOP row sits in wholesale and retail industries, because a tax on a product is remitted by whoever sells it while market shares place it with whoever makes it. The pairs are stark: petroleum wholesalers 424700 are 88,362 published against 13 estimated while refineries 324110 are 397 against 92,893; motor vehicle dealers 441000 are 45,947 against 2,301 while assemblers 336111+336112 are 26 against 24,141. The whole tax moves one stage up the chain. This is measured in the benchmark year, against the table the mix comes from, so it is not the "2017 ratios drift" objection the 2026-08-17 decision was argued from - it is stronger. A conversion that fails in 2017 cannot be rescued by being applied nearer to 2017. T00SUB fails differently and more usefully: its residual is two named structures, not a smear. S00203 other state and local enterprises is 19,471 published against 1,964 (transit operating subsidies) and S00102 is 6,339 against 102 - subsidies paid to an operator rather than attaching to a product, which no product-side operator can place. T30800 already carries them. One piece converts exactly: published T00TOP on 4200ID is 38,513 against a Supply MDTY of 38,507, so customs duties are a lookup rather than an allocation - 5.1% of the row, free, every year. The decision stands: the industry distribution stays free for Step 5 under economy-wide soft targets. But free is not unseeded, and the better operator is the margin structure rather than the Make matrix - a commodity's wholesale and retail margins say which trade industries handle it, which is the point-of-sale signal the tax follows. Those margins are built, so that operator is testable the same way. Left as the open follow-up. Findings carried as a --check flag rather than unit tests, per the convention for analysis modules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ADME Three passages still read as though T00TOP/T00SUB were simply out of Step 2's scope, which was the overstatement the previous commit corrected in the Step 2 section but not elsewhere: - the framework status table said "Not sourced", where the truth is that they are built by commodity and the industry row is a conversion Step 5 solves; - the board-provenance note said they "left Step 2's scope entirely"; - the data-gap section already said "Step 2 still supplies a seed" and was right all along - it now also carries what the seed can and cannot contain, since the market-share operator was measured and fails at r=0.20. Adds tax_axis_conversion.py to the analysis README index. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ersion Only within wholesale, and only for one code. The previous commit left "ride the margin structure instead of the Make matrix" as an untested proposal. Testing it turned up something better: Step 4c already computes the operator this needs. top_by_level splits TOP per commodity into producer-level (325,829) and trade-level (391,096) from an identity with nothing modelled in it - excise sits in Producers' Value, sales tax inside the margin columns. That is exactly the producer-versus-seller distinction market shares get wrong, and its trade-level total lands within +2.2% of published wholesale-plus- retail T00TOP. market share on all TOP + MDTY corr 0.204 |err| 114.6% + level split, trade-level by trade output corr 0.743 |err| 41.9% + motor fuel routed to 424700 by name corr 0.946 |err| 29.9% On the question itself, the answer differs by block: - Non-trade industries are 44.3% of the row and need no matrix at all. Once the producer-level portion is separated, plain market shares give corr 0.987. - Within retail, no. Output shares - which for a trade industry are very nearly margin shares - give 0.744. Retail product tax is general sales tax: broad-based, HHI 0.137, an effective 7.3 of 9 industries carrying it. - Within wholesale, yes, and output shares are worse than useless: corr -0.192. 424700 petroleum wholesalers takes 51.3% of wholesale product tax on 3.4% of wholesale output, because wholesale tax is motor fuel excise rather than a broad-based tax. HHI 0.321, an effective 3.1 of 10. But wholesale does not need a commodity-by-trade-industry matrix either. It needs one named routing: NAMED_TAX_LINES already carries motor fuel as 324110 and trade_level_share already says that tax is 99.8% trade-level, so sending it to 424700 takes wholesale from -0.192 to 0.973 and the whole row to 0.946. With petroleum pulled out by name the other nine wholesale industries score 0.825 on output shares - they behave like retail. So the general commodity-by-trade-industry margin matrix that the PRO:PUR producer-price work will eventually need is not required here. Still a seed and not a target: 29.9% absolute error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Do we need to differentiate trade industries within wholesale and within retail?Pushed Testing the proposed margin operator turned up something better: Step 4c already computes what this needs.
By block
Published
Retail tracks output; wholesale does not, and one code is why. The useful part: wholesale needs a named routing, not a matrix
So the general commodity × trade-industry margin matrix that the PRO:PUR producer-price work will eventually need is not required here. The tax conversion is served by the level split, which exists, plus a handful of named-line routings, which are already enumerated. Still a seed and not a target — 29.9% absolute error. |
Construction was the right last block to check and the wrong one to worry about. It is block-shaped like the trade industries and could have been a second petroleum; it is the opposite. Commodity TOP is 1,907 against a published T00TOP of 1,857 and plain market shares give corr 1.000, |error| 31 = 1.7%. The reason is the Make matrix. BEA defines the construction industries by type of structure, so the block is 94.5% in-block and 100.0% diagonal - there is no producer-versus-seller distinction when whoever builds the structure sells it. top_by_level agrees: 100% of construction TOP is producer-level, so the routings that rescue wholesale are inert here and all three operators give one number. MDTY and SUB are zero on both axes, so there is no duties or subsidy question in the block at all. Tax sits on 3 of 12 codes, none a NAMED_TAX_LINES entry. The residual 1.7% is a leak, and market shares get its direction right and its size about half: 5.5% of construction output is own-account or secondary work (531HST 20,279, state and local government 19,991), and published moves 50 off the block where market shares move 30.4. That answers the larger question of whether the remaining sectors each need this treatment. They do not - the error is 20 industries, not 402: top 5 industries carry 35.1% of the error, top 20 carry 80.3% 17 of those 20 are wholesale or retail wholesale 22.8% of the row 41.9% of the error retail 27.8% 33.7% non-trade 44.3% 24.4% 4200ID 5.1% 0.0% What is left is the within-trade allocation already characterised plus five named non-trade structures: 721000 accommodation (-6,592), 517210 wireless (-4,310), 221100 electric power (-3,934), and government enterprises handed tax they do not carry (S00202 +3,439 against a published zero, GSLGE +1,698, both of which belong to the Step 7 reallocation). So build against the seed and repair the named twenty later - Step 5 balances these cells under soft targets anyway, and seed accuracy below the block level is not what the build is waiting on. Adds construction_codes, construction_scores and error_concentration, wires both into the report, and adds five assertions to --check: construction stays usable, its error stays under 10%, its Make block stays diagonal, it stays MDTY/SUB-free, and the top-20 error share stays above 60% - that last one fails if the residual ever spreads and a broader sweep becomes necessary after all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
BEA books zero taxes on production to all ten government industry codes - T00OTOP and T00TOP are both zero on every one of them in 2017, the single exception being 538 of T00TOP on S00203. The columns are real (V00100 and VABAS are populated), so the zero is an accounting rule and not a gap: a tax levied by government and remitted by a government producer nets out. The market-share leg was violating that rule by 10,513 against a published 538, because government genuinely does produce taxed commodities and market shares hand it a share of the tax on them: S00202 S&L electric utilities 3,439 against a published 0 S00203 other S&L enterprises 2,428 against 538 GSLGE S&L educational services 1,698 against 0 GSLGO S&L other services 940 against 0 S00101 federal electric utilities 854 against 0 market_share_matrix gains an exclusion that drops those columns and renormalises each commodity over the producers that remain, so the tax stays with its own commodity rather than being deleted or smeared economy-wide. That is the fourth operator in the progression: + motor fuel routed to 424700 by name corr 0.946 |err| 29.9% + government columns zeroed, renormalised corr 0.948 |err| 27.9% It moves 15,692 of error, and only two thirds of that is the excess itself. The rest lands where it belongs: 221100 electric power |err| 3,934 -> 443 721000 accommodation |err| 6,592 -> 5,088 622000 hospitals |err| 763 -> 296 Read 221100 against S00202: the previous operator was under-attributing private electric power by almost exactly the amount it had misplaced onto the government utility, so the money was misplaced in a recoverable direction. The non-trade block goes from corr 0.987 / 24.4% of the error to 0.992 / 18.8%, and no government code appears in the top-20 error ranking any more. Only the producer-level leg needs the exclusion - the trade-level leg lands on wholesale and retail, where no government code sits, and duties land on 4200ID. No commodity is stranded: no commodity produced entirely by government carries any product tax in 2017, so the fallback that keeps such a row unrenormalised never fires, and the seed total is unchanged to the dollar. This one was worth fixing before the build rather than after. The other residuals are misallocations Step 5's balance can pull back; these columns are redistributed into private industries by the Step 7 government-enterprise reallocation, so a wrong seed here propagates into work that would have to be unpicked. Adds government_industries and government_scores, and four assertions to --check: the published rule still holds, the seed puts nothing there, the exclusion still reduces the row error, and renormalisation still moves money between industries without creating or destroying it. _frames is now cached - the scoring functions call it a dozen times over and --check had grown to over two minutes; it is 9s now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| df = df.query('Table_and_Line.str.contains(@table)').reset_index(drop=True) | ||
| df['TableId'] = df['Table_and_Line'].str.split(':', expand=True)[0] | ||
| df['Line'] = df['Table_and_Line'].str.split(':', expand=True)[1].astype('int') | ||
| # Match the table id exactly. This used to be a second `str.contains`, |
There was a problem hiding this comment.
I recommend avoiding comments that refer to what the code used to do
# Conflicts: # bedrock/analysis/nowcasting/plan.md
…#737) Two tests in test_nowcast_product_taxes.py have been red on nowcast since #691 merged, but #691 did not cause them: a dispatched CI run on 7827a9e, nowcast immediately before #691, fails with the same two tests and a bit-identical -26278.087320966762. The cause is #734 x #733. #733's last green run (26acafc) did not contain #734 -- `git merge-base --is-ancestor a57b0ca 26acafc` is false -- so the -26,153 band was fit on pre-#734 data. #734 then remapped the Census NAICS 2022 activities onto the goods Crosswalk, which moves the Trade FBS, purchaser_base and so the residual's annual shares. Each PR was green alone; neither was ever built with the other. Re-fit tobacco to -26,278, 0.48% off the old centre against a 0.38% band. The 2017 anchors -- the control totals, NAMED_TOTAL_2017, RESIDUAL_TOTAL_2017 and the 391,162 trade-level split -- all still pass, so the level is intact and it is the shares that moved. The divergence also grew rather than fell, which the test's own docstring names as the safe direction. test_the_two_levels_add_back_to_the_column was a separate, latent bug. producer_level is defined as TOP - trade_level, so .equals asserted that (a - b) + b == a bit-exactly, which floating point does not guarantee; it had been passing on luck. Compared with rel=1e-12 instead. Grouped the four fitted bands into two dict comparisons so a future source change reports every band that moved in one run rather than only the first. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes #536, and reshapes the Step 2 plan ahead of #538.
Draft — the plan revisions want review before the FBS methods get built on them.
Scope: three rows, and one of them is a conversion question
Step 2 builds three rows.
SUT_VALUE_ADDED_CODESand theuse_va_detail_sutdiagnostic already targetV00100,T00OTOP,V00300— theVABAScomponents.T00TOP/T00SUBare not a sourcing question. They are already built by commodity in Step 4d (#690), so the industry row is a transformation of money that exists, not a second estimate. What was open is whether the transformation works — now measured for 2017 rather than assumed (tax_axis_conversion.py).Using the benchmark market-share matrix from the Supply table,
D[c,i] = V[c,i] / T007[c]:T00TOP(TOP + MDTY)T00TOP(TOP only)T00SUBT00TOPsits in wholesale and retail industries, because a tax on a product is remitted by whoever sells it while market shares place it with whoever makes it:424700324110441000336111+336112The whole petroleum tax moves from wholesalers to refineries; the whole vehicle tax from dealers to assemblers. The operator is not noisy, it is aimed at the wrong stage of the chain.
That decision therefore stands — the industry distribution stays free for Step 5 under economy-wide soft targets — but free is not unseeded:
4200IDtakesMDTYexactly: 38,513 published against a SupplyMDTYof 38,507. Customs duties are a lookup, 5.1% of the row, every year. Independently confirms the identity already listed in the Step 5 target set.T00SUB's residual is two named structures, not a smear —S00203transit subsidies (19,471 vs 1,964) andS00102(6,339 vs 102) are paid to an operator rather than attaching to a product, so no product-side operator can place them.T30800already carries them.nowcast_trade_margins, Build the trade margin column, wholesale and retail (#613) #684), so that operator is testable exactly as this one was. Open follow-up.#536 — the tables
31 tables added to
BEA_NIPA.yaml, all annual and complete for 2012–2024, at ~950 extra FBA rows per year: controls (1.10, 1.1.5, 1.3.5, 1.14), compensation (6.2D, 6.3D, 6.10D, 6.11D, 3.10.5, 7.18), gross operating surplus (6.12D–6.22D, 7.5, 7.7, 7.9, 3.8, 7.3.5, 7.4.5, 7.11), and the QCEW-gap set below.test_bea_nipa_value_added_tables.py(17 tests) is the acceptance check: not "the tables extract" but "the numbers the block is built from come back correct through the FBA". Every expectation is a published 2017 figure, so a silent BEA vintage change fails here rather than inside Step 2.Three findings that changed the plan:
V00100within 3. Reading line 1 is what left the ~10,600 the compensation plan carried as an open item; it is the rest-of-world adjustmentA4187C, stated in 6.2D's own lines 97–99.T61600D,T71100andT11400also restate a code; select by line in all four.A108RCexactly.The QCEW-to-NIPA wage gap
QCEW covers 93.8% of NIPA wages paid. The plan treated the other 6.2% as a residual to spread pro rata. NIPA states it.
7.18 closes it exactly: BLS 7,968,336 + misreporting 106,273 + not-UI-covered 399,801 + timing 0 = 8,474,410. And it is non-uniform before any industry table is opened — government's uncovered rate is 11.3% against private's 3.5%.
6.4D turns the assumption into a measurement. QCEW publishes employment on the same axis, so QCEW/NIPA is a coverage ratio per industry. 2017, private employment, thousands:
Coverage is 97–100% across most of the economy and collapses in exactly two places — religious and grantmaking organisations are largely UI-exempt. So QCEW growth is trustworthy broadly and the exceptions are a named short list, not a flat 6.2% haircut.
Three lookups located:
RfHhInstCompW151RC= 18,684, the SUT's814000to the dollar — the sector QCEW covers worst is the one NIPA hands over directly.U32500splits general government compensation into wages and supplements, which 3.10.5 only totals.T60600Dwages-per-FTE is the plausibility check Phase 5 lacked: a share can be positive, sum to one, and still imply an implausible wage.Two parser defects the new tables exposed
Scale and unit were assumed, not read. A flat
* 1000000withClass/Unithardcoded to Money/USD is right only while every declared table is in millions of dollars. 6.4D/6.5D are thousands of persons and 6.6D is a ratio, so 6.4D's 150,654 thousand employees would have been published as 1.5e11 USD. Both now come from each series'MetricName/DefaultScale, with a map that raises on an unrecognised metric rather than defaulting to dollars. Every BEA dollar table hasDefaultScale = -6, so all 1,812 dollar rows are bit-identical across the change. 1.14's three chained-dollar lines move toClass: Other, so aClass: Moneyselection cannot add real dollars to nominal.Table ids were matched by substring. Asking for
U70205also returnedU70205S's 44 physical-quantity and price-index series under their own id. Nothing downstream selected them so it never showed — the metric guard above is what surfaced it. Matched exactly now.Plan reshape, ahead of #538
Step 2 is settled as three FBS methods, one per Use row — not one method, not plain Python modules.
BEA_Detail_Use_SUTalready melts the Use SUT throughVAPRO, so the 2017 benchmark VA block by industry is an attribution source today; no new extractor. Three rather than one becauseV00100carries a hard group-level constraint in Step 5 whileV00300/T00OTOPare seeds only, andV00300's ~20 activity sets should not destabilise the one row Step 5 constrains.multiplicationdoes not preserve the group total and there is no renormalise step. Recorded as the design seam — Phases 1–2 build a cachedFBS_outside_flowsamoved-share source, Phase 3 is a singleproportionalattribution against it, and the old Phase 3.4 exact rescale then holds by construction.Also corrected in the plan: the #567/#568 crosswalk gate is closed (three CSV lines now, not a decision); and the construction claim was too strong — the crosswalk carries 236 NAICS
23*rows, so the pipeline will not fail, it will produce an even split and call it an answer.QCEW cache
Generating the
BLS_QCEWFBA is slow (~9M rows/year, county grain). The cached per-year parquets work with no code change: copy one intoextract/output_data/andgetFlowByActivityfinds it on the ordinary local path — esupy matches on name and ignores thev2.0.4tag. Verified for 2017. Two things flagged rather than deferred: the cache is 2017–2023, so the nowcast's 2024 has no QCEW; andEmployment_common.yamlcannot be reused — every_bls_selection_fields_*block hardcodesClass: Employment.Verification
NIPA_final_dom_uses_2017rebuilds bit-identical — all 17 columns, same row count — across every change here.tax_axis_conversion --checkgreen.utils/config/settings.pyare pre-existing and Windows-only (resourcemodule); confirmed present on a clean tree.🤖 Generated with Claude Code