Skip to content

Declare period coverage on Belgium's Eurostat FX and FPB outlook facts - #402

Merged
MaxGhenis merged 2 commits into
mainfrom
be/fx-fpb-period-coverage
Nov 3, 2026
Merged

MaxGhenis merged 2 commits into
mainfrom
be/fx-fpb-period-coverage

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Microcosm-BE's money normalization (plan step B4b) reads two kinds of Chronicle fact through its feed-operand resolver: the 2024 USD/EUR annual average, as a feed-selected rate, and FPB outlook rows, as growth factors. That resolver requires both period_coverage endpoints on each fact, and compares them with the coverage the declaration authors.

In the pinned BE consumer artifact, 70,686 of 87,112 facts carry coverage. These two packages' 1,055 facts carry none. This PR adds it, and changes no fact key or value.

What changes

  • packages/eurostat/ert_bil_eur_a_usd: build_package.py now writes calendar-year coverage on all 55 record sets (basis calendar, label YYYY), and the committed YAML is its output. The notes cite only labels in the capture itself: freq=A, statinfo=AVG, JSON-stat time.
  • packages/fpb/economic_outlook_2026_2031_june_2026: each of the 1,000 record sets covers its calendar year.
    • 2022–2025 rows (400) take basis calendar.
    • 2026–2031 rows (600) take basis projection_horizon, as the ONS household projections do. The committed PDF names "la période de projection 2026-2031".
    • The package has no generator, so a one-off script inserted the blocks. A YAML reload then matched all 1,000 record sets to the originals except for the new key.
  • tests/test_source_be_period_coverage.py: 14 tests.

Invariants (tested)

  • Every fact in both packages carries coverage from 1 January to 31 December of its period year, and its label is that year.
  • Each note cites its own column. Every FPB note starts "Column headed {year} in", and every FX note names "time period {year} (JSON-stat time={year})".
  • In FPB, calendar goes with observation and projection_horizon with source_projection, exactly: 400 and 600. Each note ends on its own side of that split: "a year before the FPB's 2026-2031 projection period." for 2022–2025, and "a year in the FPB's own 2026-2031 projection period." for 2026–2031.
  • Coverage is never identity:
    • every fact's aggregate_fact_key, semantic_fact_key, legacy_fact_key and value match a digest taken at 2c7e835, at build years 2023 and 2026;
    • a separate test gives each fact arbitrary coverage and checks that the keys stay put.
  • layout.record_set_spec_hash moves on these rows, because it hashes the whole compiled record set (chronicle/sources/specs.py:820-826). No fact key reads it, and no Microcosm source code does.

Evidence

  • Tests at 03ee1e3: the new file and test_source_eurostat_exchange_rates.py give 27 passed.
  • Ablation: with the FPB YAML reverted to main, the 4 FPB coverage tests fail and the key-identity tests pass.
  • Note ablations at 03ee1e3. Each fails the new file and passes the file at 827504b:
    • a 2025 FPB note that claims the projection period: 1 failure;
    • a 2026 FPB note that cites column 2027: 2 failures;
    • the FX 2024 note naming period 2023: 2 failures.
  • Validation: validate-package and build-suite pass for both packages at 2023 and 2026, with 0 errors.
  • Against the pinned BE artifact: a pre-change rebuild of these 1,055 rows matches it exactly. The post-change rows differ only in period_coverage and the spec hash.
  • Ruff 0.14.10 (uv.lock): clean.
  • The local run used a sparse checkout, so the full suite runs in CI.

Microcosm's read

Microcosm's factor-operand validation takes a closed coverage set (start_date, end_date, basis, source_period_label and notes) and requires both endpoints (reference_factors.py:302-326). The resolver then checks each listed field against the fact's coverage for equality (:378-380). Run through _operand as it stands on Microcosm main c3b9b736, against this PR's 2024 FX row (at 827504b; 03ee1e3 changes only the test file):

The declaration lists Result
both endpoints resolves to 1353/1250, that is 1.0824
both endpoints and basis: calendar resolves to 1353/1250
all five fields resolves to 1353/1250
basis: projection_horizon refuses: "Factor operand period coverage mismatch."
end_date: 2024-12-30 refuses with the same message
an unknown field, label refuses: "Unknown factor operand coverage fields: ['label']."

So the comment at tests/test_source_be_period_coverage.py:42 holds.

#403 should land right after this PR. On these rows alone, Microcosm's roll-forward check groups the 1,000 FPB rows into 9 series keys, and 6 of them mix rows of one table; #403's body has the numbers.

Not in this PR

  • The BE consumer artifact rebuild and Microcosm's feed repin. A release holds the host's disk until about 11/05, and both follow then.
  • An FPB generator (review finding 3). A one-off script wrote the coverage blocks, so the next FPB vintage has nothing to regenerate from. A follow-up adds a builder.
  • 2025 typing in FPB (existing, unchanged here; review finding 4). The annex says FPB takes INAMI's technical estimates for 2025 health care, and it calls 2025 public debt "estimée". The package types its three 2025 T24 health-care rows observation, as its header states. This PR follows the existing typing, and a source-fidelity follow-up takes up the question.

Judges

ledger-source-fidelity and ledger-boundary: an independent review at 827504b passed both (review). 03ee1e3 applies its findings 1 and 2, and a delta review of 827504b..03ee1e3 follows.

🤖 Generated with Claude Code

Microcosm-BE's money normalization reads the 2024 USD/EUR annual average
and FPB outlook rows through its feed-operand resolver, which requires both
coverage endpoints on the fact. Neither package declared any.

Each Eurostat ert_bil_eur_a USD record set now covers its calendar year
(basis calendar), generated by build_package.py. Each FPB June 2026 outlook
record set covers its calendar year, with basis calendar for 2022-2025
observations and projection_horizon for FPB's 2026-2031 projections.

Coverage enters no fact key: all 1,055 facts keep their aggregate, semantic
and legacy keys and values, pinned by digest at 2c7e835.
layout.record_set_spec_hash moves, as it hashes the whole compiled record set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

MaxGhenis commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Review of #402 at 827504b: APPROVE
ledger-source-fidelity: PASS, ledger-boundary: PASS

The head is still 827504bac96bca036f306b9f7f3336473cb986fe, the PR is open, and CI (Chronicle checks, surface and bundle) is green on that head. I set up as CI does (git fetch …, git checkout --detach 827504ba, uv sync --locked --all-extras), and all three commands succeeded. I committed, pushed and edited nothing.

Findings, ranked (none blocking)

  1. Non-blocking: the tests don't tie the FPB notes' wording to the basis. At tests/test_source_be_period_coverage.py:120 the test only checks that "2026-2031 projection period" appears in each FPB note. A 2025 row whose notes say "a year in the FPB's own 2026-2031 projection period" would still pass. I checked all 1,000 record sets directly with a script. The 400 calendar/observation rows (2022–2025) all say "a year before…", and the 600 projection_horizon/source_projection rows (2026–2031) all say "a year in…". So the data is right today, but nothing pins it. Fix: in test_fpb_basis_follows_the_observation_projection_split, assert "a year before" in notes when the period is before 2026 and "a year in the FPB's own" in notes otherwise.
  2. Non-blocking: the note check at tests/test_source_be_period_coverage.py:73 is weak. str(fact.period.value) in coverage.notes would pass if the notes cited the wrong column, as long as the right year appeared anywhere in the text. I checked directly: every FPB note starts Column headed {period} in. Every measure's expected_column_header and source_column_id also equal the period (0 mismatches in 1,000). Every FX note names its period twice. Fix: assert notes.startswith(f"Column headed {year} in") for FPB, and assert that f"time period {year} (JSON-stat time={year})" appears in the FX notes.
  3. Non-blocking: the FPB block has no generator. The 6,000 YAML lines were inserted by a script that was then deleted (PR body). The tests cover the result, so this doesn't affect fidelity, but the next FPB vintage has nothing to regenerate from. Fix (follow-up): commit a small builder, or note the insertion rule in the package README.
  4. Non-blocking, and not caused by this PR: the 2025 typing question. See "2025 typing" below. Fix (follow-up issue): decide whether the three 2025 T24 health-care rows, which rest on INAMI technical estimates, should be typed source_projection.

Verified correct

FX fidelity (db/data/eurostat/ert_bil_eur_a_usd/ert_bil_eur_a_USD.json). I parsed the capture.

  • Its dimensions are freq {A: "Annual"}, statinfo {AVG: "Average"}, unit {NAC}, currency {USD: "US dollar"} and time 1971–2025 (55 categories). The dimension is named time, not TIME_PERIOD, as the notes say.
  • The value at time=2024 is 1.0824.
  • Every quoted label in the notes ("freq=A 'Annual'", "statinfo=AVG 'Average'", "JSON-stat time=YYYY") appears verbatim in the capture.
  • The capture doesn't spell out "1 January to 31 December". A freq=A period labelled YYYY means the calendar year under SDMX convention, and the notes never claim the dates come from the capture. I consider calendar and the Jan 1–Dec 31 dates fully supported.
  • Running build_package.py at head rewrote the YAML and manifest, and git status stayed clean, so the generator reproduces the committed bytes. test_source_eurostat_exchange_rates.py passes (it is part of the 27 below).

FPB fidelity (db/data/fpb/economic_outlook_2026_2031_june_2026/).

  • In DATA_FOR_MLT_FR.xlsx, row 3 of all six sheets (T01, T06, T07, T11, T17, T24) holds bare integer years. No column carries an estimate, provisional or projection marker, and I searched every text cell for estim, prévision, projection, réalis, provisoire, (e) and (p). The only hit is T06's title, "estimations à prix courants", which refers to current-price estimates, not to the period.
  • The labels 'YYYY' match those headers.
  • The measures are all flows or annual averages: T07 is captioned "En milliers (moyennes annuelles)", and the rest are growth rates, rates and annual € account amounts. No point-in-time stock (such as debt) is in the package, so a full calendar-year span is correct for every row.
  • The PDF (pdftotext) supports the projection_horizon boundary for 2026–2031 in FPB's own words: "Sur l'ensemble de la période de projection 2026-2031" (p. text line 268, repeated at 1246 and 1332), and "première année de la période de projection (2026)" (line 1148). The notes say nothing the captures don't support.

2025 typing (brief item 2): it doesn't block. The annex does say the health-care projection "reprend les estimations techniques de l'INAMI pour les années 2025 et 2026", and it calls 2025 debt "estimée" (debt isn't in this package). One sentence even says the government's health-care norm "évolue durant la période de projection : elle est égale à 2,5% pour l'année 2025". But period_coverage.basis records what span the period covers and how it is labelled; it is not the assertion type. calendar on 2025 asserts only that the column covers calendar 2025, which is true. The observation/projection claim stays where it already was, in assertion: observation and the package header (source_package.yaml:1), and this PR leaves both untouched. Revisiting it is a separate source-fidelity follow-up.

Identity (brief item 3).

  • I wrote my own dump script and ran it against a git worktree of base 2c7e8357 and against head. Both runs build consumer_fact_rows for both packages at build years 2023 and 2026.
  • The digest of the sorted [source_record_id, aggregate_fact_key, semantic_fact_key, legacy_fact_key, value] rows is 747005719c…5c73 (FX) and 3ba96f0e…2b4b (FPB) at both commits and both years. That matches KEY_DIGESTS.
  • I then diffed every flattened field of every consumer row, base against head (55 and 1,000 rows per year, with the same source_record_id sets). The only differences are the five period_coverage fields and layout.record_set_spec_hash (55 and 1,000 rows). The base rows carry no coverage at all.
  • record_set_spec_hash (chronicle/sources/specs.py:820) is stored on FactLayout (chronicle/core.py:237), written to a plain layout_record_set_spec_hash column (chronicle/database.py:298), and mentioned in the year-restamp docstring (chronicle/source_package.py:2187). Nothing in chronicle/ uses it as a key, primary key or join. build_fact_key and the aggregate and semantic keys don't read layout hashes or coverage.
  • test_fact_keys_ignore_any_period_coverage also passes.

Boundary (brief item 4).

  • Coverage here records only the publisher's own column year and FPB's own projection horizon.
  • Nothing ages, uprates, aligns, reconciles, imputes, activates or builds solver targets. No value, assertion or concept moves.
  • period stays the publisher's reference period. Nothing in this PR appears on AGENTS.md's list of what Chronicle must not do.

Validation and tests (brief item 5).

  • validate-package returns valid: true with no warnings for both packages at 2023 and 2026. I ran it via chronicle.harness.main, since python -m chronicle has no __main__.
  • pytest tests/test_source_be_period_coverage.py tests/test_source_eurostat_exchange_rates.py: 27 passed.
  • I ran ablations in a scratch worktree at head:
Ablation Result
FX 2024 end_date set to 2024-12-30 3 failed
FX: every coverage block stripped 4 failed
FPB: one 2025 record set's basis set to projection_horizon 1 failed
FPB: one 2031 label set to '2030' 2 failed
FPB: one projection row's assertion flipped to observation 3 failed, including both key-digest tests
  • The every-fact, both-build-year checks cover each invariant the PR body lists, except the note wording (findings 1 and 2).

Couldn't verify

  • Microcosm's resolver (brief item 6). This session's GitHub access covers only PolicyEngine/chronicle. gh api repos/PolicyEngine/microcosm/contents/…reference_factors.py?ref=ba341404… returned HTTP 403: GitHub access to this repository is not enabled for this session, so I couldn't read reference_factors.py:302-380.
    • On the Chronicle side, every consumer row's period_coverage holds exactly {start_date, end_date, basis, source_period_label, notes} as strings, with start_date <= end_date. The 2024 FX operand reads 1.0824 with start_date: "2024-01-01", end_date: "2024-12-31", basis: "calendar".
    • So a validation listing start_date, end_date and basis with those values can match, provided the resolver compares strings for equality. The owner should confirm that against Microcosm.
    • As the build lane notes, a validation that also lists notes must copy the text exactly.
  • The test's Microcosm claim. I also couldn't verify the comment at tests/test_source_be_period_coverage.py:42, "Microcosm's factor-operand validation accepts exactly these coverage fields".
  • The full suite and the BE consumer artifact. I didn't run the full suite locally; CI ran it and it is green. I didn't rebuild the BE consumer artifact, which the PR leaves out of scope.

Generated by Claude Code

…ction split

The review of #402 found that two note checks would pass a wrong note:
`str(year) in notes` passes a note that cites the wrong column while the year
appears elsewhere (2026 sits inside "2026-2031"), and the FPB basis test only
looked for "2026-2031 projection period", which a 2025 note claiming the
projection period also holds. Each FPB note now has to start with its own
column header and end with the clause for its side of the split, and each FX
note has to name its own JSON-stat time period.

Ablations, each on one fact: a 2025 FPB note claiming the projection period, a
2026 note citing column 2027, and the FX 2024 note naming period 2023. Each
passes the old file and fails the new one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Nov 3, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Review of #402 at 03ee1e3: APPROVE
ledger-source-fidelity: PASS, ledger-boundary: PASS

This reviews the delta 827504ba..03ee1e37.

  • The PR is open at 03ee1e37405caf351e053fb6220c99a49ccab370. git diff --stat shows the commit touches only tests/test_source_be_period_coverage.py (+19 −2), so no package, capture or chronicle/ code changed, and my fidelity and boundary findings at 827504b still stand.
  • When I posted, Chronicle surface and Chronicle bundle were in_progress, and the check-runs API didn't yet list Chronicle checks for this head. CI green is still outstanding.
  • I committed, pushed and edited nothing.

Findings, ranked

None blocking. Findings 1 and 2 from the 827504b review are closed.

  1. Closed: finding 1 (note wording vs. basis). test_fpb_basis_follows_the_observation_projection_split now requires each FPB note to end with ; a year before the FPB's 2026-2031 projection period. for 2022–2025, and ; a year in the FPB's own 2026-2031 projection period. for 2026–2031. I tested this in a scratch worktree at 03ee1e3:
    • a 2025 note changed to "a year in the FPB's own…" fails 1 test (the split test);
    • a 2026 note changed to "a year before the FPB's…" fails 1 test (the split test).
  2. Closed: finding 2 (each note cites its own column). The new _cites_its_own_column requires FPB notes to start Column headed {year} in and FX notes to contain time period {year} (JSON-stat time={year}). The old check (str(year) in notes) was weaker than I first said: every FPB note contains "2026" (from "June 2026" and "2026-2031"), so for 2026 rows it checked nothing. Ablations:
Ablation New file File at 827504b
A 2025 FPB note says column 2024 but still contains "2025" 2 failures not run alone; see the next row
W1 + W3 + W4 together: a 2025 note says "in the projection period", a 2024 note cites column 2023, the FX 2024 note says period 2023 5 failures 14 passed
A 2026 FPB note cites column 2027 2 failures 14 passed
The FX 2024 note names period 2023 and keeps "2024" elsewhere 2 failures not run
The FX 1999 note says JSON-stat time=1998 2 failures not run
  1. Non-blocking: no new defect. ruff check and ruff format --check are clean on the new test file.

The new checks accept every committed note

  • At 03ee1e3 the new file and test_source_eurostat_exchange_rates.py give 27 passed.
  • I also wrote a script that applies both new predicates directly to the committed YAMLs, outside pytest: 1055 of 1055 notes pass (1,000 FPB and 55 FX).

PR-body claims Chronicle alone can settle

Checked and true:

  • "03ee1e37 changes only the test file" (git diff --stat).
  • "14 tests": the parametrized count is still 14.
  • "27 passed".
  • "Ablation: with the FPB YAML reverted to main, the 4 FPB coverage tests fail and the key-identity tests pass." I reran it at 03ee1e3 and got exactly 4 failed, 10 passed. The four are every-fact ×2 years, consumer rows, and the split test.
  • "Note ablations … each fails the new file and passes the file at 827504b." I reproduced the 2026→2027 case (2 failures against 14 passed), and my own equivalents of the 2025 and FX cases agree.
  • The "Invariants (tested)" bullets on note wording match the new assertions word for word.
  • "The committed [FX] YAML is its [builder's] output." I showed this at 827504b, and the YAML hasn't changed since.
  • validate-package is valid at 2023 and 2026. I showed this at 827504b, on the same package bytes.
  • "1353/1250, that is 1.0824": exact, checked with Fraction(1353,1250) == Fraction('1.0824').
  • Spec hash: it hashes the whole compiled record set (chronicle/sources/specs.py:820-826), and no Chronicle fact key reads it (shown at 827504b).

Not checked (Microcosm-side; this session can reach only PolicyEngine/chronicle):

  • The whole "Microcosm's read" section: the closed five-field coverage set and both-endpoint requirement (reference_factors.py:302-326), the field-by-field equality check (:378-380), and the six-row _operand table. As a result, the claim that "the comment at tests/test_source_be_period_coverage.py:42 holds" also rests on that unchecked read. On the Chronicle side, these rows' coverage carries exactly those five keys, with 2024-01-01, 2024-12-31 and calendar for the 2024 FX operand. So the table's outcomes are consistent with the facts, but I didn't execute them.
  • Microcosm's ref. The section cites Microcosm main c3b9b736, while the original brief pinned ba34140449e2. I can't tell whether the resolver lines differ between them.
  • "No Microcosm source code" reads layout.record_set_spec_hash.
  • The Give each FPB outlook annex row its own source concept #403 paragraph: 9 series keys, of which 6 mix rows of one table. Give each FPB outlook annex row its own source concept #403 is a separate PR, which I didn't review.
  • The build-suite pass and the pinned-artifact comparison. I didn't rerun these, and the package bytes are unchanged since 827504b.

Generated by Claude Code

@MaxGhenis
MaxGhenis marked this pull request as ready for review November 3, 2026 07:06
@MaxGhenis
MaxGhenis merged commit f27f833 into main Nov 3, 2026
3 checks passed
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

The Belgium session (local session 0a5932a8) merged this as f27f8331, a merge commit, pinned to the reviewed head 03ee1e37 with --match-head-commit. At that head, gh pr checks exited 0 on Chronicle checks, surface and bundle, GitHub reported the PR mergeable and clean, and no review requested changes.

Independent cloud reviewers approved it twice: comment 6594854995 at 827504ba, and comment 6595222710 on the delta to 03ee1e37, which touches only tests/test_source_be_period_coverage.py.

@MaxGhenis
MaxGhenis deleted the be/fx-fpb-period-coverage branch November 3, 2026 07:07
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.

1 participant