Repository navigation
Give each FPB outlook annex row its own source concept - #403
Conversation
Every fact in FPB's June 2026 annex carried a table-wide source concept (fpb.economic_outlook_2026_2031.t07.published_cell), so a consumer that reads the observed concept as series identity took different rows of one table for one series. Microcosm's roll-forward check does exactly that: it accepted total population over working-age population as one series. Each fact now names its table and row, as Statbel's packages do (fpb.economic_outlook_2026_2031.t07.total_population). A one-off script rewrote the 1,000 source_concept lines; a YAML reload matched every record set to the original except for that field. FPB's aggregate and legacy fact keys move, because both hash the observed concept. Semantic keys, values and period coverage don't move. The period-coverage digest test now restores the table-wide concept before hashing, so it still proves coverage moved no key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of #403 at 5926dfc: APPROVE The head is still Findings, ranked1. The PR body understates which derived fields move (non-blocking)
2.
|
| Commit | Series | Series that mix canonical rows | Year coverage |
|---|---|---|---|
827504ba |
6 | 6 | — |
5926dfc1 |
100 | 0 | each series covers exactly 2022–2031 |
So under the PR, two different FPB rows give different observed concepts, and one row gives one concept across 2022–2031. This holds if Microcosm keys series on the observed source concept and nothing coarser. Someone with Microcosm access should confirm that against its code.
Verified and correct
- Fidelity against publisher bytes, all 1,000 record sets:
- I opened
db/data/fpb/economic_outlook_2026_2031_june_2026/DATA_FOR_MLT_FR.xlsxwith openpyxl myself (I didn't rely on the package guard). - For every record set, the column-A cell at
row_numberequalsexpected_row_header. - The new concept equals
fpb.economic_outlook_2026_2031.<sheet_name.lower()>.<value_id>. - The canonical
conceptends in the samevalue_id. - Each of the 100 concepts maps to exactly one (sheet, row_number, header, spec).
- Result: 0 problems.
- I opened
- All 10 T07 rows checked by hand:
total_population→ r4 "I. Population totale (II+III)"working_age_population→ r5 "I.bis Population d'âge actif (15-64 ans)"domestic_employment→ r9 "IV. Emploi intérieur"- private and public employees and self-employed → r10, r12, r11
bfp_unemployment→ r14 "VI. Chômage (concept BFP)"- registered jobseekers → r15
- older non-jobseekers → r17
bfp_unemployment_rate→ r18 "Taux de chômage (VI)/(III)"
- All 21 T11 rows and a sample of T24 checked by hand. Examples:
t11.interest_paid→ r14 "Intérêts payés (-)";t24.social_security_cash_private_sector_pensions_total→ r10 "a. Pensions du secteur privé". All match. - Row grain is more faithful than
…published_cell. The old label named no publisher series: one table-wide label stood for 4–55 distinct rows. - The form follows Statbel's convention:
<publisher>.<release>.<series>, for examplestatbel.population_structure.populationandstatbel.fiscal_income.total_net_taxable_income. - Identity: semantic keys, values and
period_coverageare identical at827504baand5926dfc1, at build years 2023 and 2026 (see finding 1 for the derived fields that do move). - Chronicle readers:
concept_alignments(chronicle/database.py:271-290,:812-866) uses(source_concept, canonical_concept, relation, legal_vintage, period)as its primary key. Before, the table-wide concept fanned out to 100 canonical concepts; now each pair is 1:1. Either way the rows are unique, so no primary-key collision appears and none disappears._observed_concept_key(chronicle/bundle.py:1389) only drivescounts.by_observed_concept. That count now gets 100 buckets instead of 6, which is the intended change.- Nothing else in
chronicle/reads the FPB concept, and no other file refers topublished_cellexcept this PR's test helper.
- The period-coverage test restoration is sound (
tests/test_source_be_period_coverage.py:123-137):- It rewrites only
measure.source_concept, and only on FPB-prefixed facts, so any other key input that moved would still change Declare period coverage on Belgium's Eurostat FX and FPB outlook facts #402's digest. That digest still matches, which also shows the spec-hash change feeds no key. - Rows → table is many-to-one, so the restoration alone could hide a row swap. But this PR's
SEMANTIC_DIGESTandROW_KEY_DIGESTpin that independently, and ablation A2 tripped them.
- It rewrites only
- Ablations, run in a separate worktree:
- A1: I relabelled one 2025 cell,
t07.total_population→t07.working_age_population. 7 new tests fail; the semantic and coverage tests pass. - A2: the consistent swap described in finding 2. 6 tests fail.
- A3: I reverted the YAML to
827504ba. The same 7 tests fail. - These match the PR body's ablation claims.
- A1: I relabelled one 2025 cell,
- Focused tests: I ran
tests/test_source_fpb_row_source_concept.py,tests/test_source_be_period_coverage.pyandtest_belgium_targets.py::test_fpb_annex_has_requested_table_counts_and_vintage_boundary: 24 passed. - Validation and lint:
validate_source_packagereports 0 errors at 2023 and 2026. Ruff check and format are clean on the three test files. - Boundary: this is a pure relabelling of the publisher row. No code changes, and no reconciliation, aging, imputation, support-aware activation, solver construction or PolicyEngine-computed values.
- The
imputed_social_contributionshits in the diff are FPB's own row labels ("Cotisations sociales imputées"), not Chronicle imputation. assertion(observation for 2022–2025,source_projectionfor 2026–2031) andperiod_coverageare unchanged.
- The
Not verified
- Microcosm's
_rolled_forward_series_keyand_source_measure_conceptcode (finding 3). - The full test suite locally. CI ran it and passed.
Generated by Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… header ROW_KEY_DIGEST now also covers observed_measure_key and concept_alignment_key, the other two keys that hash the observed concept. Each stays unique per annex row. The module docstring names every field that moves, the record set's spec hash included. A new test ties 21 (sheet, row id) pairs, all ten T07 rows and a sample of every other table, to the publisher's column-A header: once through the expected_row_header the package guard checks, and once through each fact's groupby_value_label. Swapping ids and concepts between T07 rows 4 and 5 now fails this test by name, not only the digests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of #403 at 0c1c6ee: APPROVE The head is still When I posted this, CI on Findings, ranked1. The header pin can't tell apart T24 rows that share a header (non-blocking)
2.
|
|
The Belgium session (local session 0a5932a8) retargeted this to main after #402 merged, then merged it as Independent cloud reviewers approved it twice: comment 6594859598 at |
Stacked on #402. Merge #402 first; GitHub then retargets this PR to
main.Every fact in FPB's June 2026 outlook annex carries a table-wide source concept, such as
fpb.economic_outlook_2026_2031.t07.published_cell. A consumer that treats the observed concept as series identity then takes different rows of one table for one series.Microcosm's roll-forward check (
_rolled_forward_series_key,ledger_targets.py:3484-3505) does exactly that. A Microcosm triage accepted a 2025 total-population numerator over a 2024 working-age-population denominator as one series, with a ratio of 1.59, inside its bound. Microcosm's hub ruled this a fix for Belgium's Chronicle adapter, not a shared change (B3 triage, claim 5).What changes
packages/fpb/economic_outlook_2026_2031_june_2026/source_package.yaml: each of the 1,000 record sets now names its table and row, in Statbel's style:fpb.economic_outlook_2026_2031.t07.total_population. That gives 100 concepts, one per published row, each spanning 2022–2031.source_conceptlines.tests/test_source_fpb_row_source_concept.py: 11 tests.tests/test_source_be_period_coverage.py: the digest test restores the table-wide concept before hashing, so it still proves that Declare period coverage on Belgium's Eurostat FX and FPB outlook facts #402's coverage moved no key.tests/test_belgium_targets.py: the per-table count now reads the concept's table prefix.Invariants (tested at build years 2023 and 2026)
fpb.economic_outlook_2026_2031.<sheet>.<row id>. The sheet comes from the package's own record set, and the row id equals the layout'ssource_row_id.DATA_FOR_MLT_FR.xlsx: all ten T07 rows and a sample from every other table. Each pair'sexpected_row_headerand every fact'sgroupby_value_labelequal that header. The package guard already refuses a fact whose header cell differs (chronicle/sources/specs.py:260-267,:541-547), so the pin ties the id, and with it the concept, to the publisher's text.source_concept(chronicle/consumer_contract.py:129-138,:797-804). The aggregate key (:178) and the alignment key (:247) nest the observed-measure key, and the legacy key hashes the whole measure (chronicle/core.py:638).Evidence
Tests at 0c1c6ee: the new file, the period-coverage file, the Eurostat FX file and
test_belgium_targets.py::test_fpb_annex_has_requested_table_counts_and_vintage_boundarygive 39 passed.Belgium suite:
test_belgium_targets.py -k "fpb or bfp"gives 3 passed. The one failure isbfp-economic-outlook-2026-06, missing from the sparse checkout ("Source package not found").Ablations at 0c1c6ee, over the new file and the period-coverage file:
value_idandsource_conceptbetween T07 rows 4 and 5 in all ten years, a mislabel consistent with itself (the review's A2), fails the header test at both build years. It also fails both digests in the new file and Declare period coverage on Belgium's Eurostat FX and FPB outlook facts #402's coverage digest: 8 failures in all.Before and after, over all 1,000 facts at build year 2026, 7 fields differ, each in every fact:
aggregate_fact_keyandlegacy_fact_key;observed_measure.source_conceptandobserved_measure_key;concept_alignment.source_conceptandconcept_alignment.concept_alignment_key;layout.record_set_spec_hash.The semantic key,
source_series_key, the value, lineage andperiod_coveragedon't move. The review found the same 7 at build years 2023 and 2026.Validation:
validate_source_packagepasses at 2023 and 2026, with 0 errors.Ruff 0.14.10: clean.
The local run used a sparse checkout, so the full suite runs in CI.
Microcosm's read
Microcosm's roll-forward check keys a series on the observed source concept. It reaches the canonical concept only when a fact has no source concept (
_source_measure_concept,ledger_targets.py). Run through_rolled_forward_series_keyas it stands on Microcosm mainc3b9b736, the 1,000 FPB rows give:827504ba)5926dfc1)With this PR, each of the 100 keys spans 2022–2031 with no repeated year. The YAML hasn't changed since either head. So #402 and this PR should land back to back: #402 alone gives Microcosm both the coverage it needs and series keys that mix rows.
Downstream
Microcosm-BE pins FPB's aggregate keys and source concepts in its B3 reference documents under
microcosm/build/be/(target_references.json,holdout_references.json,reference_inventory.jsonand others). None of them pins the observed-measure key, the alignment key or the spec hash. Those documents regenerate when Microcosm repins its feed to the next BE consumer artifact. That one rebuild picks up both #402 and this PR. A release holds the host's disk until about 11/05.Judges
ledger-source-fidelityandledger-boundary: an independent review at 5926dfc passed both (review). A delta review of 5926dfc..0c1c6ee follows.🤖 Generated with Claude Code