Repository navigation
Add Belgium's target, precalibration and holdout reference documents - #1442
Conversation
Lift the documents from Chronicle's Belgian bundle suite through a generator, pin the consumer artifact, and keep Belgium inactive. Active rows are demography and commune support; every other family stays parked with its reason. A package test keeps calibration and holdout facts separate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Point the shared legacy-selection test at an active Belgian row, which fixes engine-free CI. - Map each subnational vintage to its typed authority. Chronicle's NUTS_2024 can't be a typed alias, so the row metadata and the inventory carry the mapping and a package test enforces it. - Replace the money tripwire: a non-diagnostic factor may live only in the reference profile's context, and every money node must select it. - Bind each row and factor to its resolved feed keys in the inventory, so separation no longer depends on how a selector is spelled. Partition totals count as read, which catches a holdout on B.1's income total. - Flag rows by value, not key, when a target equals FPB's projection: the D61, D62 and D5 holdouts do too. - Record the FPB T24 line choice and cut claims the suite can't source. - Refuse a band whose upper sits off a class edge, and restore B1's required families as a package test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The no-Python-entrypoints scan read 646 fact keys in `resolved_reads` as `dotted.module:callable` strings. It admits fact keys only under named fields, so each key now sits in an `aggregate_fact_key` or `semantic_fact_key` object. The scan stays as strict as it was. Add the generator to the closed set of `lexical_floats` callers. It reads the feed's lexical tokens for its ageing pairs, as NZ's ageing tool does. CI's `--maxfail=1` hid both failures; a no-stop run of the engine-free list found them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The inventory's digest and the package fingerprint move, and nothing else in the golden does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sol agreed at 989ce9b with six non-blocking findings. This commit makes the two guard fixes and the four text fixes, and merges #1447. Guards: - The holdout-measure check now keys on the measure alone. A holdout column reused on another entity passed before. - A pin check ties each aggregate key a row or factor selector pins to its own resolved-read entry. The digest binds an entry to its selectors but not to the keys it lists, so a rewritten selector with a recomputed digest kept its old keys. Roster selectors name no key, and only the feed audit proves what they resolve to. - Four new mutants cover these, and each goes red with its guard removed. Text: - The overlap notes say ONSS scales with wages, not "a fixed share", and drop the "line 3" label on gross wages, which sits on an unnumbered row under that line. - grammar_gaps names the second tolerance change: the ONSS row moves from B1's standard_admin_release 10% to 5%. - Correction to c700edd's message: the generator needs the lexical_floats opt-in for the RSZ factor operands that resolve_reference_factor reads, not for its FPB pairs, whose 24 operands are JSON integers. The set membership stands. The inventory text moves, so the golden repins its fingerprint and the inventory hash. No row or number changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Opus delta review at 40601e4 ran a holdout rewritten to the ONSS calibration fact with a null aggregate key and a null measure. The resolver treats a null field as no filter, so the row compiled, and every artifact-free check passed. Other fact keys and mapping-form dimensions did the same. - Each selector now keeps to the committed fields, leaves none null or empty, and pins an aggregate key or, for a base selector, a roster. - Each resolved-read entry must carry its own document's reference set, so relabelling an entry can't hide a holdout's keys. - The operand mutant now rewrites the whole operand, so it models a read. Ten spellings on the base and operand selectors and two relabelling mutants cover this, and each goes red with its guard removed. No committed selector trips the new checks, and no document, row or number changes. Corrections to 40601e4's message: ba2590a merged #1447, not that commit; it made three text fixes plus the PR-body fix for finding 6; and it also rewrote the inventory's "Separation is checked" line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Opus delta review at 2ec5084 ran a calibration row rewritten to a partial roster: the D62 holdout's selector with one of its seven dimensions named as a one-member list. The resolver checks only the pins a selector names, so the row read the holdout's fact, while the cell check built a different cell and every artifact-free test passed. No committed selector does this. Each selector now pins exactly the dimensions it declares, with at most one non-empty roster, and no null or empty member at any depth. Twelve more spellings cover this, and every clause of the check now has a case that goes red with that clause removed. These shapes catch hand edits. The feed audit stays the proof, because a roster's members bind to nothing without the artifact, and the PR body now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test's operand kept the committed empty dimensions, so the dimensions clause refused it and the index guard it names stayed untested. The operand now declares the roster's dimension, and the test goes red with that guard removed. The docstring also says a roster's members bind to no listed key, since the cell check does read them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Landed as dc0785f (squash), pinned to the reviewed head d0cb3f6.
|
|
Ready for hub review Main SHA:
1. Schema-2 profile requires
|
| Spec | Rows it would refuse |
|---|---|
| nz | 285 (64 regc_2025, which also isn't in NZ's alias set; 221 with no vintage) |
| uk | 550 (no typed layers) |
| am | 6 |
| be | 18 (NUTS_2024) |
| us | 0 |
So turning the rule on for schema 1 is not a small fix.
Smallest fix (Belgian-only). Keep geography_vintage_bindings and its test. Inferred alternative: if the nuts1 layer really is NUTS 2024, set the authority value in be/spec/sources.yaml:27-29 to NUTS_2024. Then test_country_spec.py:838-882 and test_be_target_references.py:803-804 go red and need updating.
Shared alternative, not recommended. A declared aliases: list on vintage records. With it, NUTS_2021 declared as an alias of a 2025 layer would pass.
8. Holdout guard: CONFIRMED, wider than claimed
Reproduction 1, composition. scratch/c8/repro_toy.py builds make_composed_fixture, whose holdout is (person, receives_main). It appends a calibration target_references row and calls compose_transport_graph:
same column (person, receives_main): ACCEPTED, 27 nodes
other entity (household, receives_main): ACCEPTED, 27 nodes
Reproduction 2, packaged copies. scratch/c8/repro_pkg.py copies the packaged spec, adds the row to target_references.json, and calls load_transport_spec(Path):
be same column (person, employment_income): ACCEPTED
be other entity (household, employment_income): ACCEPTED
nz same column (family, accommodation_supplement_positive_payment_indicator): ACCEPTED
nz other entity (household, …): ACCEPTED
Mechanism. The gap is wider than the claim. holdout_columns (T/compose.py:1811) guards only pre-calibration stage outputs and modules (:1915-1918). Nothing compares calibration or pre-calibration reference rows with holdout rows, on any key:
validate_transport_activation(T/compose.py:1153-1180) checks each reference document's activation separately.- The ancestry checks (
T/compose.py:2431, 2492, 2644, 2676, 2706) refuse a calibration-ancestry node that selectsholdout_referencesor names a holdout source. They don't refuse a row intarget_referencesthat measures the holdout's column. load_transport_spec(:543-570) only loads.country_spec._validate_target_references(:817) validates one document at a time.
Today only Belgium's package test enforces separation: _separation_violations, test_be_target_references.py:194-223, mutants at :576-577. It keys on the measure alone, across the calibration, pre-calibration and holdout sets. Serial run: -k "separated or holdout_measure", 3 passed.
Classification. Shared gap. NZ is exposed the same way.
The key. Measure on any entity, across every non-holdout reference document: calibration, pre-calibration and mass.
scratch/c8/scan.pyruns both keys against every packaged reference document (am, be, nz, uk incl.local_target_references, usfiscal_target_references). Under either key, no spec is refused. Only BE and NZ haveholdout_references.- No packaged spec uses one measure on more than one entity, in any document, so the stricter key costs nothing today. No packaged spec has a legitimate person total beside a household total of the same concept.
- Caveat: under the measure key, a future holdout on a generic count measure (
people,households,tax_units) would refuse every demographic row that uses it. No holdout does that today. If one is ever needed, it should get its own measure column, as NZ's indicator holdouts already do.
Smallest fix.
- Guard: in
validate_transport_activation, refuse any row in a reference resource other thanholdout_references(the set atT/compose.py:1153-1163) whosemeasureis a holdout row'smeasure. - Optional: switch the stage guard at
:1811to the measure key too. - Sequencing: both live in
T/compose.py, which is under edit in G8-w4a: release-input evidence producers and their checked gate #1449–G8-w4d: wire the release-evidence producers into the NZ graph and toy #1452, so the hub sequences them after those PRs land. - Interim: the same cross-document check fits in
load_country_spec(country_spec.py:2324, wherepayloadsholds every package document). It isn't under edit, and it covers packaged and local-directory specs. It does not cover in-memory specs handed straight tocompose_transport_graph, as the toy fixture's are. - Tests that go red: none among packaged specs, from the scan. Inferred, not run: the toy fixtures in
test_support/microcosm_build/transport_{composed,nz_toy,entitlement}.py, which need the same check before landing. - Tests needed: the two reproductions above as refusals (same column, other entity), a pre-calibration and a mass variant, and a stage-output-on-another-entity variant if
:1811changes.
Risk. The rule applies only to declared holdout_references documents. UK and US holdouts are row-level by design: the UK's rotated holdout folds (changelog.d/1301-uk-dense-opt-in-holdout-incumbent.changed.md) and the US ACS district blocks (changelog.d/us-acs-local-state-cd-sparse.added.md) hold out rows of measures that are calibrated elsewhere. Neither country has a holdout_references document, so the guard can't touch them. A "rule for every country" must not be read as banning row-level holdouts.
Left out
- Belgium's B4 and later packages.
- Any guard beyond what the eight claims need.
- The receipt shape and validity window of the claim-2 operation.
- Whether CREATE accepts a composed national mass row (claim 6, next step).
- Chronicle's Belgian adapters themselves.
- A full-suite run.
Rulings the hub owes
- Claim 8, holdout separation key and home. Adopt the measure key across all non-holdout reference documents. Pick an interim
country_spec.pyguard now, or aT/compose.pyguard after G8-w4a: release-input evidence producers and their checked gate #1449–G8-w4d: wire the release-evidence producers into the NZ graph and toy #1452. Max: a holdout rule declared for every country spans methodology. Confirm it is scoped to fact-levelholdout_referencesand leaves row-level rotated holdouts (UK, US) alone. - Claim 2, annual ratio-pair operation. Decide whether to build it. Max: may cuts and their band amounts age by different ratios? The question spans countries.
- Claim 1, rolled rows under schema 2. Max: may Microcosm-computed roll-forwards coexist with schema 2's "projection only in Chronicle" posture? NZ (221 rows) and the UK use the operation. If yes, take the pinned-base fix in
country_spec.py:1323. If no, Belgium stays on schema 1. - Claim 4, signed roll-forward. Take the one-line fix at
ledger_targets.py:1157. Hub only. - Claim 6, composed-row labels. Take the declared-name fix in
ledger_targets.py, and decide whether the catalog must equal the declared name where it has an entry. Hub only. Belgium can bind FPB's national fact meanwhile. - Claim 7, subnational vintage rule under schema 1. Max: extending it would refuse 285 NZ, 550 UK and 6 AM rows today. Belgium keeps its package test meanwhile.
- Claims 3 and 5: no shared change. Route both to Belgium's Chronicle adapters (operand
period_coverage; row-grain FPBsource_concept).
Generated by Claude Code
Plan step B3 of the Belgian transport port, in
~/reviews/belgium-2026-10-12/transport-port/PLAN.md.A lane built this. Sol's first review asked for changes, and Sol's second agreed at 989ce9b with six non-blocking findings, which 40601e4 fixes. Three Opus delta rounds then found gaps in the artifact-free selector checks, which 2ec5084, 5739c51 and d0cb3f6 tighten. They changed no committed document. Every row stays
proposed_pending_d1902. Belgium keeps refusing to activate, and nothing here publishes a number.What changes
tools/generate_be_target_references.pywrites Belgium's reference documents from Chronicle's Belgian consumer artifact, whichbe/chronicle_feed.jsonpins: 87,112 facts,facts_sha256e01a4630…,manifest_sha25678397926…, source commit 2c7e8357f (Chronicle #401's merge).New under
be/:chronicle_feed.json,precal_references.json,holdout_references.json,reference_aging.json,reference_intervals.json,reference_profiles.json,reference_inventory.json. The generatedtarget_references.jsonreplaces B1's placeholder groundwork.be/country_package.jsonlists the seven new resources, andcatalogs.yamladds the twoeurcolumns. B1's document moves to a test fixture, so the shared schema-2 tests still run on Belgian geography.This PR changes no file under
build/transport/,spec_engine/orframe/. It edits these shared tests:test_legacy_selection_path_changed_mutant_frozen_surface_params_and_keys[be]read B1'sspf_finances_pit_totalrow, which this PR removes, so it now reads an active demography row.test_lexical_float_opt_in_is_limited_to_factor_consumersholds a closed set oflexical_floatscallers, and the new generator joins it, astools/generate_nz_reference_aging.pydid.resolve_reference_factorneeds the opt-in for RSZ's float operands. The 24 FPB pair operands are JSON integers, so the tokens don't touch them.test_country_spec.pydeletestest_target_profile_declares_tiers_and_income_basis, because the schema-1 profile drops those tiers, and points the schema-2 vintage, placeholder and catalog-unit tests at B1's fixture.test_spec_engine_country_bundles.pylists the seven new resources and the two neweurcolumns.Rows
Every parked row names its reason. All three B.1 checks fail on the documents (tax definition, income ranking, policy year), so B.1 stays inactive.
Invariants
reference_profiles.factor_context_resource, and everymoney.normalize@1node, in the graph or in entitlement rows, must select that resource.ageing_target_equalsexactly when its base value equals its pair's denominator, which the feed audit checks.--checkregenerates the documents byte for byte, given the artifact.CI has no artifact, so the 20 feed-audit tests skip there and run locally. CI's engine-free job stops at its first failure, so the whole list also ran locally with no stop at a828a22 and the artifact loaded: 24,076 passed and 228 skipped. The 13 files that run couldn't finish, because they need the repo's import mode or the orrery package, then gave 304 passed on their own. Two later merges took #1440 and #1447. At d0cb3f6,
--checkpasses, and the five test files this PR touches give 407 passed and 2 skipped, the golden test among them (both need the NZ artifact):test_be_target_references.py,test_country_spec.py,test_ledger_artifact.py,test_spec_engine_country_bundles.pyandtest_transport_reference_selection.py. All of that covers Python 3.14 only.Choices the card doesn't fix
~/reviews/belgium-2026-10-12/transport-port/jobs/b3-references/d1902-choices.mdlists 17, including the band list, the ratio-pair series, FPB T24's unemployment line (a. at €5.602bn, not 4. at €6.51bn), theNUTS_2024mapping and what the schema-1 profile drops. Those go with d1902, and none of them makes a row active that the card doesn't already allow.Shared gaps
Sent to the NZ hub, which queued them as shared work after w4, with no shared change here:
rolled_forward_by_ratio, so Belgium uses NZ's schema-1 profile.rolled_forward_by_ratiorefuses a negative base.Limits
🤖 Generated with Claude Code