Skip to content

fix(nowcast): hold the 336414/33641A see-saw the 2022 census invents (#862) - #874

Merged
WesIngwersen merged 7 commits into
margins-purchaser-anchor-872from
ec-conditioning-corroboration-862
Sep 8, 2026
Merged

fix(nowcast): hold the 336414/33641A see-saw the 2022 census invents (#862)#874
WesIngwersen merged 7 commits into
margins-purchaser-anchor-872from
ec-conditioning-corroboration-862

Conversation

@WesIngwersen

@WesIngwersen WesIngwersen commented Sep 7, 2026

Copy link
Copy Markdown
Member

Closes #862. Stacked on #873, at the top of the trade/margins stack.

The evidence needs no threshold

336414 and 33641A are one see-saw, and the decisive fact is that the census and BEA agree almost exactly on the pair's combined output and disagree entirely about the split:

2022 gross output, $M 336414 33641A pair
BEA, unconditioned 22,001 20,849 42,850
census-conditioned 16,879 25,982 42,861

The total agrees to 0.03% while 5.1 billion USD moves between the two members. That is a reallocation, not a measurement of growth — and guided missiles is concentrated enough that one firm's classification between vehicles and propulsion units moves the published split with no economic change behind it.

QCEW payroll is opposed in both halves: the census moves 336414 to 0.744× BEA and payroll to 1.226×; 33641A 1.209× against payroll's 0.766×.

What holding the pair buys

shipped pair held
336414 surplus, 2022 −2,957 +311
336414 surplus, 2024 −3,693 −22
economy-wide negative surplus, 2022 −20,952 −17,995

That is #862's headline, and the band BEA's own unconditioned series sits in.

They are held as a pair. Holding 336414 alone imposes 33641A's move and changes the family's split in a way neither source supports — which is #862's own open decision 1, answered.

⚠️ No automatic screen is shipped, and that is the considered answer

#862's decision 2 asks whether the screen should look at more than the coverage wedge. I built two constant-free rules and both fail:

Sign-flip on gross operating surplus — hold where the conditioning drives surplus negative and BEA's own series does not. Fires on zero industries in every year. 336414 is negative under both arms; the conditioning changes its magnitude roughly tenfold without crossing zero. There is no accounting-boundary rule available here.

Rank by census–payroll disagreement — dominated by payroll noise rather than census error. The largest disagreement of all 223 industries is 333991 handtools, where the census matches BEA to 4.9% and payroll runs +98.5%. 94 of 223 industries disagree in direction; the see-saw sits at 0.481 against a 99th percentile of 0.681 and does not stand out at all.

⚠️ An earlier draft of this PR used a gate on the size of the census move — the arbitrary constant this project is trying to remove. It was not derivable from anything: 0.25 flags one industry, 0.20 three, 0.15 six, 0.10 sixteen. Worse, 0.25 splits the see-saw — holds 336414, imposes 33641A — the one outcome both sources agree is wrong. The gate is gone; the decision lives in PENDING_REVIEW, a list of named industries with measured reasons.

What is in this PR

⚠️ Three unrelated changes, and the title only covers one. Review caught that the original description listed two files while the diff has four. Closes #862 and #867.

1. The see-saw hold (#862)

  • ec_go_adjustment.PENDING_REVIEW gains 336414 and 33641A, documented as one see-saw held together and why.
  • analysis/nowcasting/ec_payroll_corroboration.py — the three growth series ranked and unfiltered, for a human reviewing a census move. --check asserts all nine figures above.
  • family_reallocation() — a first cut at the generalisation this points at: which BEA groups does the census reallocate while leaving the group total alone. That is the shape of the 3364 defect and needs no magnitude constant.

2. ⚠️ The value-added residual now routes to a row the industry actually has — nowcast.py

This has the widest blast radius in the PR: it changes every industry's value-added block in every year, where the see-saw hold changes two aerospace codes. It was previously invisible in this description.

_reconcile_to_published_vapro routed every industry's reconciliation residual to V00300 gross operating surplus. Two industries have no surplus row at all. BEA publishes V00300 of exactly zero for 814000 in 2007, 2012 and 2017, with VAPRO equal to compensation to the dollar — private households earn no operating surplus, definitionally, because the sector's output is the compensation it pays. 4200ID is the same shape one row along: a synthetic code carrying customs duties.

This was a hard blocker, not a rounding matter. The routing put 1,967 $M (2018) rising to 5,104 (2024) into a cell the balance mask holds as a Tier 0 structural zero, and seven of the eight years failed outright. value_added_residual_row() now derives the absorber per industry from the published 2017 table; exactly two industries take the exception.

Pinned by a new test rather than by prose — review's catch, and a fair one, since the result was asserted in the docstring and held nowhere. The derivation's bare 0.5 is now VA_ROW_PRESENCE_ATOL with its reasoning stated (BEA publishes in millions and rounds to the unit; same bar as sut_use_to_mut_use.REPLAY_ATOL). A third industry newly qualifying now fires the test instead of being picked silently by idxmax.

3. The staleness checker (#867) — stale_artifacts.py

builder is resolved as a dotted module and used as the method surface, plus a new unverifiable class for artifacts carrying neither lineage nor a resolvable method, and --delete / --scope flags.

Review found four problems in it, all fixed here:

  • The module docstring claimed unverifiable catches the Step 5-7 products. It does notmethod_files resolves their builder in its first branch, so they land in method. Measured on this tree: --name Nowcast_Detail reports 108 method changed, 0 unverifiable. The docstring now says what the class actually catches (mostly flowsa's FBAs) and where the Step 5-7 hole is really closed.
  • --delete had no dry run. It is now a dry run listing what would go, with --yes to act, and it never deletes unverifiable artifacts — "cannot be checked" is not "known stale", and a Step 5 or 6 product removed on that basis costs hours to rebuild.
  • --delete --strict deleted and exited 0, the two joined by elif. They are independent now: it deletes and still exits 1.
  • _tracked_change_times needs full history. A shallow clone (fetch-depth: 1, the Actions default), a failed subprocess and an expired timeout all returned {} silently, which reads as "nothing is stale". All three warn on stderr now, and the docstring says to fetch full history before gating CI on --strict.

Left on the table, deliberately

325120 industrial gases is opposed too — census 1.237× BEA, payroll 0.902× — and is not held: its surplus is comfortably positive either way (4,273 million at 2022). A census move nothing corroborates, but no evidence of harm. Acting on it would be acting on the screen rather than on the symptom.

Testing

Covering all four files, not just the two the original block ran:

# 1. the see-saw hold
uv run python -m bedrock.analysis.nowcasting.ec_payroll_corroboration --check  # 9/9 PASS
uv run pytest bedrock/transform/iot/__tests__/test_ec_go_adjustment.py -q      # 6 passed

# 2. the value-added routing
uv run pytest bedrock/transform/iot/__tests__/\
test_derived_intermediate_and_value_added.py -q     # 16 passed, absorber pinned
                                                    # to {814000: V00100,
                                                    #     4200ID: T00TOP}

# 3. the staleness checker, behaviour that had no test
uv run python -m bedrock.utils.validation.stale_artifacts --name Nowcast_Detail
    # 108 problems: 108 method changed, 0 unverifiable
uv run python -m bedrock.utils.validation.stale_artifacts --name Nowcast_Detail --delete
    # DRY RUN -- lists, deletes nothing, exit 0
uv run python -m bedrock.utils.validation.stale_artifacts --name Nowcast_Detail --delete --strict
    # exit 1  (was 0 before this PR)

uv run black --check . ; uv run ruff check . ; uv run mypy bedrock             # clean

⚠️ This changes gross output for two industries in every year 2022+, so it obliges a rebuild of the value-added block and everything downstream.

🤖 Generated with Claude Code

WesIngwersen and others added 5 commits September 7, 2026 18:40
…862)

#862 found that 336414 guided missiles carries the largest unexplained
negative gross operating surplus in the value-added block - -2,957 at
2022 growing to -3,693 at 2024 - and that 86% of it is our own EC-2022
output conditioning rather than anything in compensation.

The decisive evidence needs no threshold. The census and BEA agree almost
exactly on the pair's combined 2022 output and disagree entirely about
the split:

                     336414    33641A      pair
  BEA unconditioned  22,001    20,849    42,850
  census-conditioned 16,879    25,982    42,861

The total agrees to 0.03% while 5.1 billion USD moves between the two
members. That is a reallocation, not a measurement of growth, and guided
missiles is concentrated enough that one firm's classification between
vehicles and propulsion units moves the published split with no economic
change behind it. QCEW payroll is opposed in both halves: the census
moves 336414 to 0.744x BEA and payroll to 1.226x; 33641A 1.209x against
payroll's 0.766x.

Both are now held in PENDING_REVIEW, and held as a PAIR - holding one
imposes the other's move and changes the family split in a way neither
source supports. Measured, that restores 336414's surplus from -2,957 to
+311 at 2022 and from -3,693 to -22 at 2024, the band BEA's own
unconditioned series sits in, and improves the economy-wide negative
surplus from -20,952 to -17,995 at 2022.

⚠️ No automatic screen is shipped, and that is the considered answer
rather than the lazy one. Two constant-free rules were built and both
fail:

- Sign-flip on surplus - hold where conditioning drives surplus negative
  and BEA's series does not - fires on ZERO industries in every year.
  336414 is negative under both arms; the conditioning changes its
  magnitude tenfold without crossing zero.
- Ranking by census-payroll disagreement is dominated by payroll noise,
  not census error. The largest disagreement of 223 industries is 333991
  handtools, where the census matches BEA to 4.9% and payroll runs +98.5%.
  94 of 223 industries disagree in direction; the see-saw sits at 0.481
  against a 99th percentile of 0.681 and does not stand out at all.

An earlier draft of this used a gate on the size of the census move,
which is exactly the arbitrary constant this project is trying to remove.
It was not derivable from anything - 0.25 flags one industry, 0.20 three,
0.15 six, 0.10 sixteen - and worse, 0.25 SPLITS THE SEE-SAW, holding
336414 and imposing 33641A, the one outcome both sources agree is wrong.
The gate is gone.

The new module ships the evidence ranked and unfiltered for a human
reviewing a census move, plus family_reallocation, a first cut at the
generalisation this points at: which BEA groups does the census
reallocate while leaving the group total alone. That is the shape of the
3364 defect and it needs no magnitude constant.

325120 industrial gases is opposed too and is deliberately NOT held: its
surplus is comfortably positive either way, so there is a census move
nothing corroborates but no evidence of harm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…#867)

It detected two staleness classes, both keyed on an upstream artifact's
date. Neither can see the third: the artifact's own METHOD changing.
Nothing upstream moves, so nothing flags, and re-running returns the
previous parquet silently - the FBS cache key is the git hash, so an
uncommitted edit does not change it and a committed one only moves at the
next commit.

That bit twice on 2026-09-07. A re-weighted export split returned the old
numbers verbatim; the second attempt rebuilt 2017 only and put a fake
discontinuity between 2017 and 2018 that reads exactly like a real seam.
Both times stale_artifacts reported one unrelated problem and nothing
else.

The new `method` class compares the artifact's date_created against the
newest of (a) the last commit touching its method files and (b) their
working-tree mtimes. The mtime half is not redundant - it is the only
thing that catches an uncommitted edit, which is the case that actually
happened.

Method files are the yaml, its !include tree, and the modules its
!clean_function / !script_function tags name. ⚠️ NOT every .py beside the
yaml: that was the first version and it flagged 985 of 995 artifacts,
because an unrelated commit anywhere in transform/nipa marked every
NIPA_VA_* parquet stale. A check that says everything is stale targets
nothing. Resolving the tags instead keeps the surface honest - measured
on NIPA_VA_surplus_2017 it is three files, and the flag it raises is a
true positive (BEA_NIPA.py changed 2026-09-04, the artifact was built
2026-08-28).

Also adds --delete, which removes the parquet AND its metadata - leaving
the parquet behind means the rebuild reloads it, which is the whole
problem. It refuses anything outside OUTPUT_DIRS and prints every path
first.

--scope defaults to `transform`. Extracts and transforms cost very
different things to rebuild: an FBS reruns from parquets already on disk,
an FBA re-fetches from an API or GCS. Defaulting to the cheap half keeps
it one command away and makes the expensive half deliberate.

On this tree it finds 290 distinct stale artifacts where the old check
found 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The method class I just added used max(last commit, working-tree mtime).
The mtime half is what catches an uncommitted method edit - the case that
actually bit twice - but on its own it also fires on `git checkout`,
which rewrites the mtime of every file it touches without changing a
byte.

Caught it immediately: Census_USATrade_2024 was reported as "method
changed at 2026-09-04 18:48" when the last commit touching
Census_USATrade.py was days earlier and nothing was edited. The 09-04
timestamp was a branch switch. On a stack this deep that is most of the
tree, and a staleness check that cries wolf after every checkout is a
staleness check nobody runs.

The mtime is now consulted only for files that actually differ from HEAD,
read once from `git status --porcelain`. The uncommitted-edit catch is
unchanged; the checkout noise is gone.

Measured on this tree: 995 problems -> 233, and the composition is now
meaningful. All 233 are FBAs whose extractor module was committed to
after they were built - USDA_ERS_FIWS.py, Census_AWTS.py, Census_ARTS.py,
BEA.py, Census_SAS.py, BEA_NIPA.py - with zero superseded and zero
internal, the transform side having just been cleared and rebuilt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Caught while clearing artifacts for a full rebuild. `--name Balanced`
answered "No stale cached artifacts" about balanced SUTs built 2026-09-01
that sit on trade inputs rebuilt today with materially different numbers.

The reason is that Step 5 and Step 6 products are written by bespoke
savers rather than the FBS framework. Their metadata carries `builder`,
`engine_result`, `protocol` and `step` - and NO primary_source_meta and
NO method_url. So `superseded` and `internal` have no sources to compare
and `method` has no method to resolve. Every class silently passed, on
the most important outputs in the pipeline.

Two changes:

- `builder` is now resolved as a dotted module and used as the method
  surface. That alone flags all 38 Balanced_Detail_* artifacts, via
  nowcast_sut_assembly.py changing on 2026-09-04.

- A new `unverifiable` class reports any artifact carrying neither source
  lineage nor a resolvable method, rather than passing it over. A silent
  pass on something that cannot be checked is worse than a noisy flag,
  because it reads as a clean bill of health.

⚠️ The builder surface does not give these products real lineage. They
are downstream of the trade FBS and the gross-output conditioning and
know nothing about either, so a rebuild of an input still cannot flag
them. Recording sources at save time is the actual fix and belongs with
whoever owns save_balance and save_mut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#856 reconciles the value-added block to BEA's published value added by
putting the per-industry residual into V00300 gross operating surplus.
That is right for 400 of 402 industries and impossible for two.

Found by rebuilding: seven of the eight years failed outright with

  ValueError: 1 cells are nonzero in the seed but marked structural zero,
  first at ('V00300', '814000') = 1967.07

rising to 5,104 by 2024. 2017 passed only because its residual is zero by
construction, which is why this went unnoticed - the balanced products on
disk for 2018-2024 were built 2026-09-01, four days BEFORE #856 landed,
so the reconciliation had never been exercised on those years.

⚠️ The mask was right and the routing was wrong. Private households earn
no operating surplus, and that is definitional rather than a 2017
accident: BEA publishes V00300 of exactly zero for 814000 in all three
benchmark years - 2007, 2012 and 2017 - with VAPRO equal to V00100 to the
dollar each time. The sector's output IS the compensation it pays; there
is no enterprise, no capital and no margin. 4200ID is the same shape one
row along, a synthetic code carrying customs duties, so its value added
is a tax on products (T00TOP 38,513, everything else zero).

Exempting the mask cell instead would have booked billions of household
operating surplus and let it into every downstream coefficient.

value_added_residual_row() derives the absorber per industry from the
published 2017 table: V00300 wherever it exists, otherwise the row that
actually carries that industry's value added. Exactly two industries take
the exception, and both are single-row.

Measured: 814000 now reads V00100 27,382 at 2024 against a published
VAPRO of 27,382, with no surplus - previously V00100 22,278 plus a
V00300 of 5,104 that cannot exist. 2018 balances at T11 max |residual|
0.0 $M where it raised before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-862

Carries the four new commits from main up the stack. No conflicts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bl-young

bl-young commented Sep 8, 2026

Copy link
Copy Markdown
Member

The diff carries two changes the description does not mention.

The "What is in this PR" list covers ec_go_adjustment.py and ec_payroll_corroboration.py. The diff has four files, and the other two are both load-bearing:

Each of the three is a good change and I am not asking for any of them to be dropped. But the see-saw hold, the value-added routing fix and the staleness checker have nothing to do with each other, and only one of them is reviewable from the description. The Testing block has the same gap: it runs test_ec_go_adjustment.py and the corroboration --check, neither of which touches nowcast.py or stale_artifacts.py.

Please update the description and the Testing section to cover all four files, and reference #867 the same way #862 is referenced. The value-added routing fix in particular needs its own paragraph: it changes every industry's value-added block in every year, which is a much wider blast radius than holding two aerospace codes, and right now a reader would not know it is in here at all.

@bl-young

bl-young commented Sep 8, 2026

Copy link
Copy Markdown
Member

The routing fix has no test, and its failure mode already cost seven of eight years.

bedrock/transform/iot/__tests__/ has no reference to value_added_residual_row or _reconcile_to_published_vapro. That is the one place in this diff where I think a test is genuinely owed rather than paranoid, for three reasons:

  1. The failure mode is recorded history, not hypothesis. The docstring says it plainly: seven of eight years failed outright with "1 cells are nonzero in the seed but marked structural zero" because the residual was routed to V00300/814000. A regression here does not raise -- it either fails the balance a rebuild later, or routes billions to a row that should be zero and lets it into every downstream coefficient.

  2. The result is asserted in prose but pinned nowhere. The docstring states that exactly two industries take the exception and names them (814000 -> V00100, 4200ID -> T00TOP). Nothing in the code or the tests holds that. It is derived at runtime from the published 2017 table.

  3. The derivation is not obvious from reading it. An industry keeps V00300 if abs(block.loc['V00300', industry]) > 0.5; otherwise the absorber is carried.idxmax(), the largest-magnitude row it does carry. That 0.5 is a bare constant, and idxmax() will silently pick some row for any industry that newly qualifies -- there is no "I do not know" branch. Worth noting the irony that this PR's own argument against the census-move gate is that arbitrary constants are what the project is trying to remove.

Suggested test, one failure mode, no restating of the implementation:

def test_only_two_industries_take_the_non_surplus_absorber() -> None:
    """814000 and 4200ID carry no gross operating surplus; everything else does.

    Routing a residual to V00300 for these two puts value in a cell the
    balance mask holds as a structural zero.
    """
    absorber = value_added_residual_row()
    exceptions = absorber[absorber != 'V00300']
    assert exceptions.to_dict() == {'814000': 'V00100', '4200ID': 'T00TOP'}

If the published table is ever revised such that a third industry qualifies, that fires and a human looks at it, which is the outcome you want. It is also cheap -- the function is functools.cached and reads a table the suite already loads.

@bl-young

bl-young commented Sep 8, 2026

Copy link
Copy Markdown
Member

stale_artifacts.py: unverifiable does not do what its docstring says, and --delete has no safety.

Four things in this file, in descending order of how much they matter.

1. The unverifiable class documents a case it does not actually catch. The module docstring introduces unverifiable with the Step 5 and Step 6 products as its motivating example -- "written by bespoke savers that record a builder and an engine_result but no sources", and "before this class existed the checker answered 'No stale cached artifacts' about balanced SUTs built weeks earlier than the trade inputs underneath them."

But method_files() resolves builder in its first branch and returns the module path, so files is non-empty for exactly those artifacts. The unverifiable branch requires not files, so the balanced SUTs and the MUT quartet never reach it -- they land in method instead, flagged when their builder module moves. method_files()'s own docstring describes this correctly ("Resolve the builder so at least a change to it is visible"); the two accounts disagree.

That is worth fixing because the module docstring is what a reader consults to decide whether a flag is real. It should either describe what unverifiable actually catches (artifacts with no builder, no cornerstone-data/bedrock method_url, and no sources -- flowsa's FBAs, mostly) or the resolution order should change. Note also that routing them to method does not close the original hole: a change to the trade inputs underneath a balanced SUT still moves nothing that this check can see, because the builder module did not change. That is the same gap #875 flags as remaining, so the class as written does not fix it and the docstring implies it does.

2. --delete has no dry run and no confirmation. It removes the parquet and the metadata for every flagged artifact in scope, defaulting to transform. #875 reports 386 flags standing after the rebuild, including 108 superseded versions of the nowcast quartet which live in transform/output_data. Everything is recoverable from GCS, but a one-word flag that silently takes out a Step 5 or Step 6 product costs hours of rebuild to undo. Suggest either defaulting to a dry run with an explicit --yes to act, or -- probably better -- excluding the unverifiable class from deletion on the grounds that "cannot be checked" is not "known stale."

3. --delete silently swallows --strict. They are joined by elif, so --delete --strict deletes and exits 0. If this ever becomes a pre-rebuild CI gate, that is a false pass rather than a loud one.

4. _tracked_change_times() needs full history. It parses git log --name-only over the whole repo. Under a shallow CI clone (fetch-depth: 1, which is the common default) almost no path gets a commit time, so method staleness under-reports silently instead of erroring. Worth a note in the docstring at minimum, and worth knowing before --strict gates anything in Actions. The 180s timeout returns {} on expiry too, which fails the same open way.

The design itself is sound -- the mtime half gated on _dirty_paths() content rather than raw mtime is exactly right, and the note about the first version flagging 985 of 995 artifacts is a good record of why the narrow resolution is the correct one.

@bl-young bl-young left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving this despite the comments above. I'd recommend they be addressed soon after we pull in this stack (if not before). But I would recommend updating the PR body at minimum.

Three findings from review, all correct.

**The value-added routing had no test.** Its failure mode is recorded
history rather than hypothesis - seven of eight years failed outright -
and the result was asserted in prose but pinned nowhere. Adds the test
that pins it: exactly two industries take the non-surplus absorber,
814000 to V00100 and 4200ID to T00TOP. A third industry newly qualifying
now fires instead of being picked silently by idxmax.

**The bare 0.5 is now VA_ROW_PRESENCE_ATOL**, with the reasoning stated:
BEA publishes the detail tables in millions and rounds to the unit, so
half a unit is where "carries no such row" stops being a rounding
artefact. Same bar as sut_use_to_mut_use.REPLAY_ATOL. Fair catch that a
PR arguing against arbitrary constants had introduced one.

**stale_artifacts, four things:**

- The module docstring claimed `unverifiable` catches the Step 5-7
  products. It does not: method_files resolves their builder in its
  first branch, so they land in `method`. Measured on this tree -
  `--name Nowcast_Detail` reports 108 method changed, 0 unverifiable.
  The docstring now says what the class actually catches and where the
  Step 5-7 hole is really closed.
- `--delete` is now a dry run listing what would go; `--yes` acts. It
  also never deletes `unverifiable` artifacts, because "cannot be
  checked" is not "known stale" and a Step 5 or 6 product removed on
  that basis costs hours to rebuild.
- `--delete --strict` deleted and exited 0, joined by elif. They are
  independent now, so it deletes and still exits 1.
- _tracked_change_times needs full history. A shallow clone, a failed
  subprocess and an expired timeout all returned {} silently, which
  reads as "nothing is stale". All three now warn on stderr, and the
  docstring says to fetch full history before gating CI on --strict.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WesIngwersen added a commit that referenced this pull request Sep 8, 2026
Carries the review fixes from #874 up the stack. No conflicts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@WesIngwersen

Copy link
Copy Markdown
Member Author

All three addressed. Thank you — the first and third were both cases where the document and the code disagreed, which is the worst kind to leave standing.

1. The description covered two of four files. Rewritten into three numbered sections, and it now closes #867 the same way it closes #862. The value-added routing has its own section saying plainly that it has the widest blast radius in the PR — every industry, every year, against two aerospace codes for the see-saw. The Testing block runs all four files now, including the staleness behaviour that had no check at all.

2. The routing fix had no test. Added yours, essentially verbatim — it passes and pins {814000: V00100, 4200ID: T00TOP}. You were also right about the bare 0.5: it is now VA_ROW_PRESENCE_ATOL with the reasoning stated (BEA publishes in millions and rounds to the unit, so half a unit is where "carries no such row" stops being a rounding artefact — the same bar as REPLAY_ATOL). Fair hit that a PR arguing against arbitrary constants had introduced one. I left idxmax as it is: the test is the "I do not know" branch, since a third industry qualifying now fires it instead of being picked silently.

3. stale_artifacts, all four. Your read of the resolution order was exactly right, and measurable — --name Nowcast_Detail reports 108 method changed, 0 unverifiable, so those products never reach the class the docstring credited. Docstring now describes what it actually catches and points at where the Step 5-7 hole is really closed.

  • --delete is a dry run; --yes acts. It also never deletes unverifiable artifacts — your framing that "cannot be checked" is not "known stale" is the right rule and I took it as written.
  • --delete --strict now exits 1. It exited 0 before, exactly as you said.
  • _tracked_change_times warns on stderr for all three silent paths (shallow clone, failed subprocess, expired timeout), and the docstring says to fetch full history before --strict gates anything.

On your closing point in 3.1 — that routing them to method does not close the original hole — that is right and it is now fixed one PR up rather than papered over: utils.metadata.source_lineage records what a saver actually read, wired into save_mut and save_after_redef. Step 5 still cannot name its inputs and #875 says so explicitly.

@WesIngwersen
WesIngwersen merged commit 9826c4d into main Sep 8, 2026
5 checks passed
@WesIngwersen
WesIngwersen deleted the ec-conditioning-corroboration-862 branch September 8, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

336414 guided missile and space vehicle manufacturing: negative gross operating surplus is our EC-2022 output conditioning, not compensation

2 participants