Conversation
…r-selecting
These packages pin artifact_year, so every --year reads the same file, but
their labels rendered {year} from --year. A --year 2023 build therefore
stamped TY2022 and TY2020 IRS tables as ty2023 (chronicle#117 item 1) and
stamped CY2024 BEA regional values as cy2023.
- soi-w2-statistics-2020: period, record ids, vintage and legal_vintage are
literal 2020.
- soi-congressional-district-2022, soi-state-2022 and both IRA 2022
packages: the same labels are literal 2022.
- The three Historic Table 2 packages: legal_vintage is literal tax_year_2022.
- ici-fact-book-table-30: vintage is literal calendar_year_2026, which is what
its artifact-year build emits.
- bea-regional-state-personal-income-components-2024 now selects
SAINC5N__ALL_AREAS_1998_2025.csv's own year column with column_by_year
(1998 = I ... 2023 = AH, 2024 = AI, 2025 = AJ), and source_column_id
follows the year. --year 2023 now reads the 2023 column: US personal income
is 23,577,208,000 thousand, not the 2024 cell's 24,897,613,000.
Every artifact-year build is byte-identical to origin/main (facts and
consumer rows for all ten packages, BEA regional at 2024 included).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
artifact_year pins which file a package reads, while each {year} label
renders from --year. Add a structural guard so a pinned build can no
longer stamp one year's publisher data as another year.
artifact_year_restamp_issues(package, year) compares the build at Y with
the build at the artifact year A. A record set whose selection is the same
at Y and A while any other rendered field differs is a restamp. Selection
means the artifact's parser, sheet, archive member and rendered
selected_rows, plus the record set's sheet, row and column addresses, value
scaling and guard cells. The other fields include period, record ids,
vintage, source_table, legal_vintage, filters, constraints and notes.
- SourcePackage.build_source_rows, build_source_cells and
build_source_record_set_specs raise ArtifactYearRestampError(ValueError).
Every other build path goes through them.
- validate_source_package reports error code artifact_year_restamp.
- A default build-bundle treats the code as "no data for this year": it
skips the package and gives the reason in skipped_sources. An explicit
--source fails the bundle (source_suite_build_failed, exit 1).
Year-selecting pinned packages still build off-year. That covers
selected_rows on {year}, column_by_year, sheet_name_by_year and guards that
follow the year. The check compiles specs only and never parses the
artifact. Over all 219 pinned packages at A-1, A+1, 2023 and every microcosm
scope year (585 pairs), it flags the same 22 pairs in 10 packages as the
design prototype on origin/main's package YAML, and 0 pairs after the
package fix.
Tests: synthetic refuse/allow/literal cases, validate and CLI exit codes,
default-bundle skip and explicit-source failure, an exhaustive property
test over 1,280 synthetic cases (Hypothesis is not a dependency), a
real-tree guard check, and a real-bytes differential: origin/main's IRA
and BEA regional YAML restamp without the guard and are refused with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
column_by_year binds each year to a column letter of
SAINC5N__ALL_AREAS_1998_2025.csv. Check the binding at build time: each
measure now expects row 1 of its year's column to name that year
(expected_column_header_by_year). A refreshed file with shifted columns then
fails loudly instead of relabelling one year's values as another year's.
A delimited file's header row keeps the text '2024', while package YAML
renders a digit-only string to the integer 2024 (_render_value). So no
year header guard could match a CSV header. _resolve_guard_cell now matches
a text cell against an integer expectation when the text is exactly that
integer's digits, and nothing looser ('02024', '2024.0' and booleans still
fail). Only builds this used to reject can change; a build that passed
before resolves the same cells.
Effect on BEA regional: each fact's source_cell_keys gains the row-1 header
cell of its column, and layout.record_set_spec_hash changes. Values,
periods, record ids, source_row_keys, aggregate_fact_key and
semantic_fact_key are unchanged at 2023, 2024 and 2025, and every year
1998-2025 builds with the guard passing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
artifact_yearpins which file a source package reads, but every{year}label still rendered from--year. So a pinned package built at another year relabelled one year's publisher data as that year. The default bundle and the microcosm US feed both did this:soi-congressional-district-2022stamped22incd.csv(TY2022) asty2023: 26,880 facts. This is item 1 of CD record sets stamp 22incd.csv as ty2023 — a tax-year vintage error; add Table 1.4 taxable-interest facts (populace#489) #117.soi-state-2022, both IRA 2022 packages andsoi-w2-statistics-2020stamped TY2022 and TY2020 workbooks asty2023. The W-2 case is the one microcosm#1029's reviewer traced.bea-regional-state-personal-income-components-2024read the 2024 column (AI) at every--year, so its "cy2023" facts were CY2024 values. For example, US personal income at cy2023 was 24,897,613,000 thousand, which is the 2024 cell. The file's 2023 cell is 23,577,208,000 thousand.historic_table_2and its two state packages stampedlegal_vintage: tax_year_2023on TY2022 facts.ici-fact-book-table-30stampedsource.vintage: calendar_year_2023on its 2026 edition.This PR does three things:
Fixes item 1 of #117 (the congressional-district stamp). Items 2 and 3 of #117, the Table 1.4 interest facts and HT2 TY2023, stay open. Context: microcosm#1029.
What changed
Harness:
chronicle/source_package.py,chronicle/bundle.py(commit 2)artifact_year_restamp_issues(package, year) -> list[SourcePackageIssue]compares the build at Y with the build at the artifact year A. It checks two things:selected_rows, plus each record set's sheet and each row's and measure's rows, columns, value scale, rounding, divisor and guard cells.vintageandsource_table.A record set whose selection is identical at Y and A while anything else differs gets an
artifact_year_restampissue. The message names the package, A, Y and the labels that moved.{year},{filing_year}or*_by_yearreturns early.ArtifactYearRestampError(ValueError)is raised bySourcePackage.build_source_rows,build_source_cellsandbuild_source_record_set_specs.build_source_regions,build_source_record_specs,build_source_recordsandbuild_factsall go throughbuild_source_record_set_specs, so they raise too.validate_source_packagereports the issues as errors with codeartifact_year_restamp. It still fills in the counts.bundle._source_unavailable_for_yeartreatsartifact_year_restampas "no data for this year".skipped_sources: "source package pins another year's artifact and would only relabel it as Y (artifact_year_restamp)".--sourcestill reachesbuild_source_suite, which raises. The bundle then recordssource_suite_build_failed, the report is invalid, andbuild-bundleexits 1.The rule is structural and names no package. Year-selecting pinned packages keep building off-year:
selected_rowswith a{year}Period (BEA NIPA, Census projections),column_by_year(CBO, CMS NHE, Z.1, JCT),sheet_name_by_year, and header guards that follow the year.Package data (commit 1)
irs_soi/w2_statistics_2020:{year}becomes2020inperiod,record_set_id,source_record_id_prefix,artifact.vintageandlegal_vintage.irs_soi/congressional_district_2022,state_2022,ira_roth_contributions_2022andira_traditional_contributions_2022: the same fields get2022.irs_soi/historic_table_2,historic_table_2_state_agi_2022andhistoric_table_2_state_broad_2022:legal_vintage: tax_year_2022.ici/fact_book_table_30:vintage: calendar_year_2026, the value its artifact-year build emits today.bea/regional_personal_income_stateis now year-selecting.column_by_year(one YAML anchor) for every year column thatSAINC5N__ALL_AREAS_1998_2025.csvhas: 1998 =Ithrough 2025 =AJ, so 2023 =AHand 2024 =AI. All 416 selected rows are numeric in all 28 year columns.source_column_id: '2024'becomes'{year}'on the 6 measures that had it. The twovalueones are unchanged.Commit 3: BEA regional year-header guard (
chronicle/sources/specs.py)column_by_yearbinds each year to a column letter. Commit 3 checks that binding at build time: every BEA measure now expects row 1 of its year's column to name the year (expected_column_header_row: 1,expected_column_header_by_year, one anchor). If a refreshed file has shifted columns, the build then fails instead of relabelling one year's values as another's.For this to work,
_resolve_guard_cellneeded one change. A CSV header row keeps the text'2024', but package YAML renders every digit-only string to the integer 2024 (_render_value), and the guard compared with!=. So no year-header guard could ever match a delimited file._guard_value_matchesnow accepts a text cell that is exactly the expected integer's digits, and nothing looser:'02024','2024.0'and booleans still fail. Only builds this used to reject can change; a build that passed before resolves the same cells.Trade-off: commit 3 breaks byte-identity for BEA regional at 2024. Each fact's
source_cell_keysgains exactly one key, the row-1 header cell of its column (AI1 at 2024), andlayout.record_set_spec_hashchanges, because the spec hash coversexpected_column_header*. At 2023, 2024 and 2025 these fields do not change: value, period, record ids,source_row_keys,aggregate_fact_keyandsemantic_fact_key. Every year from 1998 to 2025 builds with the guard passing. If byte-identity at 2024 matters more than the build-time binding check, drop commit 3 (aa3e4de). Commits 1 and 2 stand on their own; with them alone BEA at 2024 is byte-identical to origin/main.Docs
docs/agent-source-package-harness.mdhas a new "Pinned artifacts and year labels" subsection under the declarative authoring contract. It says thatartifact_yearpins the file, so labels must be literal unless--yearalso changes what is read. It also describes the guard, the validate code and the bundle behaviour. Chronicle has no changelog (changelog.d/andCHANGELOG*are absent).Why
Chronicle records each fact's publisher reference period (
AGENTS.md).docs/pe-uk-source-checklist.mdsays that "artifact_yearidentifies the publication vintage; it does not redefine the fact period." Aligning a fact to another period is the consumer's job; microcosm already ages dollar targets from the fact period. With{year}labels on a pinned file, though, the period itself was wrong.In the microcosm US feed pinned at
c5e5bf8, 27,309 of 39,158 rows were restamps: CD 26,880, BEA regional 416, W-2 5, state 4, IRA 2 + 2. For example, the W-2 tips rows labelled ty2023 carry the TY2020 file's sha256, cells and values. A dollar target aged from ty2023 therefore skips three years of wage growth. The design notes and the adversarial audit behind this PR are in the review folder that goes with microcosm#1029.Invariants
These hold for every pinned package, every build year Y and every artifact year A:
(value, source_cell_keys, source_row_keys)equals a fact of the A build while any other field differs.column_by_year,selected_rowson{year},sheet_name_by_year) are never refused, and a year their file lacks fails with its own error.artifact_year_restamp_issues(package, A) == [], and unpinned packages are never checked.The selected cells must match together with
source_row_keys: a year selected throughselected_rowslands on the same virtual cell at every year, and only itssource_row_keysdiffer.Before/after evidence
All numbers below come from scripts in the review folder (
scratch-E/), run on this branch and on a detached origin/main worktree.cmp, 20 files). With literal labels, every IRS and ICI build at 2023 is byte-identical to its own artifact-year build.source_row_keysare unchanged. Every year 1998-2025 builds, and US personal income equals the CSV row for each year.layout.record_set_spec_hashandsource_cell_keyschange, the latter by one added header-cell key per fact.Tests
New module
tests/test_chronicle_artifact_year_restamp.py(48 tests, about 15 s locally):{year}period raisesArtifactYearRestampErrorfrom all seven build entry points. The message names the package, both years and each moved label.legal_vintage, artifactvintage,source_table, a filter plus its constraint) is enough to refuse it.validate_source_packagereportsartifact_year_restampwith unchanged counts, and the harnessvalidate-packagecommand exits 1.column_by_year,selected_rowson{year}andsheet_name_by_yearread that year's value (140 rather than 110), with different lineage. A year the file lacks fails with its ownValueError.artifact_year_restampin the reason and the validation errors, and still builds a literal-label package. An explicit--sourcegivessource_suite_build_failed, an invalid report andbuild-bundleexit 1.pyproject.tomlanduv.lock), so this test enumerates the whole space instead of sampling it: 2^6 templated-label subsets x 4 selection mechanisms x 5 build years = 1,280 cases. It asserts invariants 1-3 for each case against the unguarded build.{year}/{filing_year}or has a*_by_yearkey (13 packages after this change).slowmarker convention, so none was added.ira_roth_contributions_2022andbea/regional_personal_income_stateedited back to origin/main's labels or fixed column. Without the guard, every artifact-year fact comes back relabelled; with it, the build is refused.tests/test_chronicle_source_package.py:test_bea_regional_state_personal_income_components_read_the_requested_yearchecks 2023 values against the CSV'sAHcells, the 2025 US value, and that 1997 and 2026 fail.tests/test_chronicle_source_cells.py(commit 3): a guard expecting 2024 matches the header text'2024'but not'2023','02024','2024.0'orTrue.Default-bundle snapshot: pending.
tests/test_chronicle_bundle.py::test_build_bundle_writes_merged_consumer_contractpins the whole--year 2023default bundle: fact, period, semantic-duplicate and table counts. This PR moves those counts:tax_year:2023totax_year:2022/2020;Only a full bundle build can give the new numbers (CI's test step takes about 2 h), so this PR stays a draft until they are re-pinned from that run.
Chronicle Governance
ledger-source-ingestorfor the package data (packages/**),chronicle/sources/specs.pyand the tests.chronicle/source_package.pyandchronicle/bundle.pyare outside every approved role'sallowed_paths. CODEOWNERS routes them to @PolicyEngine/core-developers for review.validate_source_packagereportsartifact_year_restampin the new tests.tests/test_chronicle_artifact_year_restamp.py,tests/test_chronicle_source_cells.pyand the new BEA test intests/test_chronicle_source_package.pypass (61 passed);ruff check chronicle testsis clean.ledger-source-fidelity: pending (main session runs them)ledger-contract: pending (main session runs them)ledger-boundary: pending (main session runs them)This PR changes the source-data boundary in one direction only. Chronicle now refuses to emit a fact at a period its publisher did not report. It does not align or age anything; that stays with consumers.
Follow-ups (not in this PR)
chronicle_feed_scope.jsonwhen it re-pins to this commit. Its 2023 run no longer produces ty2023 rows for CD, state, IRA and W-2, soselect_rowswill stop with "scoped pairs produced no row".23in54us.xlsx,23in55cmcsv.csv) and IRA Tables 5/6 (23in05ira.xlsx,23in06ira.xlsx); packaging them would give real ty2023 facts. No TY2023 W-2 or CD table exists yet.artifact_year or year. Its fact builds should go through the guardedSourcePackagebuild methods.🤖 Generated with Claude Code