Skip to content

Adopt the UK national calibration contract (re-homed from chronicle#164 per the #166 ruling) - #707

Open
juaristi22 wants to merge 10 commits into
mainfrom
uk-national-contract
Open

Adopt the UK national calibration contract (re-homed from chronicle#164 per the #166 ruling)#707
juaristi22 wants to merge 10 commits into
mainfrom
uk-national-contract

Conversation

@juaristi22

@juaristi22 juaristi22 commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Implements the chronicle#166 ruling — selection contracts live in the consumer; Chronicle is a facts-only layer — and, per the 2026-08-18 scope adjudication, now also carries #622 item 1. Three layers, one commit each class:

  1. The contract (hardened carrier)uk/uk_national_targets.json, the review-hardened chronicle#164 artifact: 186 value-free target declarations with the full policyengine-uk-data registry-parity accounting at pinned ref ebf733c. Every target is byte-identical to the chronicle#164 review state modulo the chronicle_selectorledger_selector key rename and the signed deviations below.
  2. The resolver extensionledger_targets.py's selector vocabulary learns the chronicle#164 selector semantics (dimension_values strict-typed pins, list-form dimensions, list membership, record_set_id/groupby_dimension aliases), so one resolver serves the US references, the Belgian file, and all three re-homed UK contracts (Migrate the UK local-geography and firms contracts out of Chronicle (chronicle#166 ruling) #708). US semantics unchanged; unknown keys still raise.
  3. The first activationuk/target_references.json, typed per the Belgian convention: 13 active references at period 2023 (the certified-spine year), generated from the contract and admitted only if they compile fail-closed against chronicle facts under identity. Rich binding payloads stay in the contract; references point back via metadata.contract_target_id. Nothing consumes the compiled registry yet — the build seam is Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622/Calibrate the UK national build from Ledger-backed targets #623, so activation stays inert.

Registry-parity accounting (now closed exactly)

651 rows @ ebf733c = 609 mapped + 42 signed exclusions, and the 3 contract targets with no incumbent ancestor are now signed in registry_parity.unmapped_declarations: the hmrc.salary_sacrifice.it_relief_{basic,higher,additional}_rate declarations are backed by chronicle#139 facts, while the incumbent's source CSV is 410-Gone at the pin and its registry rows are silently suppressed (bare-except drop; the build_loss_matrix.py:376-394 dispatch branches are unreachable dead code). The parity test flips from subset to exact closure: mapped ∪ unmapped_declarations == declared target_ids, disjoint, 183 distinct mapped ids.

New registry_parity.accounting_notes record what the incumbent cannot self-report:

Deviations from the chronicle#164 verbatim carry (fix-and-sign register)

Deviation Count Why
geography_levels canonicalized ["country","region"] on the voa.council_tax_stock.* targets 9 Matches the 168 single-level targets; consumers are membership-only (local_targets.py)
Issue citations normalized to repo namespaces (populace#622/#467microcosm#…; bare #133/#134/#139/#159chronicle#…) 37 strings The bare/legacy namespaces are ambiguous outside the chronicle repo
description gains the selector-vocabulary note 1 The shared ledger_selector key now genuinely interoperates via this PR's resolver extension
registry_parity gains unmapped_declarations + accounting_notes Accounting closure; microcosm-authored section, not chronicle#164 content

Deliberately not fixed (carried for parity, adjudicate in #622): the 14 metric_name/source_name prefix disagreements (uk-data registry keys carried verbatim), the all-186 bindings.axiom status: "pending" stubs, and the one string value_expression ("esa_income + esa_contrib", obr.esa) — now fenced by a closed-world binding-key test so none of these can proliferate silently.

The first active subset (13 rows) and what is deferred

Membership was determined by per-candidate compilation against the merged UK consumer-facts feed of chronicle main post-#173 (98,392 facts; sex-split MYE record sets included), with geography_level: "country" appended to each activation row's selector. Active: isc.private_school_students, the 10 ons.household_composition.* types (these exercise the new dimension_values matching), ons.savings_interest_income, ons.public_sector_employment.

Deferred, enumerated per target in the authoring run's membership report:

Class Count Meaning / owner
no_fact_at_or_before_2023 101 Fact vintages start after 2023 (all OBR EFO lines, DWP 2025/2026 releases, VOA/Scotgov 2025, the 15 TCL rows). Activation grows with #622's uprating/period declarations or the WS-E/F year move
multi_fact_sum_resolution_pending 23 Band/groupby targets whose value is a sum over matched facts (resolution_defaults.operation: "sum"); the shared compile path resolves exactly one fact per reference — #622 resolver work. Includes the CGT totals, whose 37 same-series year-rows also expose that the period-invariant key does not yet strip UK record-set year spellings
geography_ambiguous_at_country_level 49 The UK total and the four constituent countries share level country; per-target geography_id pinning is a #622 adjudication

The 33 allow_source_projection targets fall inside the classes above at 2023 (no eligible observation ≤2023); assertion-policy handling is #622's.

Tests

  • test_uk_national_targets.py: exact accounting closure; target_id uniqueness; closed-world selector keys and the 24-key bindings.policyengine vocabulary; per-kind required fields for the 3 counterfactual kinds; two-level reduce (binding any; predicate any/any_child_under/count/sum); dimension_values shape; CGT metric-name equality with UK_CGT_TARGET_SPECS (makes Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622's single-homing cutover mechanical).
  • test_ledger_targets.py: strict-typed dimension_values (int ≠ str ≠ bool), list membership, missing-dimension no-match, dimensions: [] and exact name-set semantics, both chronicle aliases, list-valued scalar keys, and a US regression on mapping-form dimensions.
  • test_uk_target_references.py: typed load (count pinned), per-row derivation parity against the contract, all periods 2023, and a compile smoke through compile_ledger_target_references on two real feed rows (ISC pupils 554,243; ONS HAXV £86.04bn) asserting identity resolution and lineage metadata.
  • Guard: uk_national_targets.json allowlisted in test_no_incumbent_data_package_references_in_live_tree under the existing sha-locked historical-reference convention.

Full suite + ruff check green locally; wheel packaging verified to ship both new JSON resources.

What this PR is NOT

It does not build the mapping module, the metric providers for the 3 counterfactual kinds, the two-fixture compile-parity gate, or the ledger_facts release-manifest pin — that is #622 (whose issue body is updated with this PR's findings); the calibration seam is #623.

Note for reviewers: @vahid-ahmadi's review verified the verbatim carrier before the scope expanded; commits 156d4df3..d3916c11 isolate everything added since, so the new surface can be reviewed incrementally.

🤖 Generated with Claude Code

Adversarial-review response (2026-08-18)

A Codex adversarial review (run locally, verdict needs-attention) challenged the active references' derivation rule: measure carries the published metric label (e.g. ons/lone_households_under_65) rather than a raw model column, and the contract's filters/conditions are not on the reference — so a naive consumer could compile the registry, resolve values, and then either skip every target (missing slash-named columns) or constrain the wrong aggregate.

Disposition — the derivation rule stands, and the semantics it relied on are now explicit and tested rather than implicit:

  • measure naming a prepared column is the US pipeline's own doctrine, not an accident: target_spec_from_ledger_reference requires a measure precisely because "count-like facts must be represented as sums of prepared indicator columns", and the JCT materializer names its prepared household columns after their targets (hh[reform_spec.measure] = reform − baseline). A raw value_variable measure would be wrong for exactly the filtered counts the reviewer cites: person_count without its filter constrains the total population, and no raw model column exists for "households of type X" — the column must be prepared from the binding payload. That preparation is the Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622 UK materializer's contract, driven by metadata.contract_target_id → the contract's rich binding (value_variable + filters/household_conditions + entity mapping), which is the single source of truth this file deliberately does not duplicate.
  • What changed in response (commit after this note):
    1. Every reference's metadata now carries "measure_kind": "prepared_column", and the file description states the prepared-column contract explicitly — no future consumer can mistake the measure for a raw model variable.
    2. The derivation-parity test now also asserts every active reference's contract binding carries the value_variable payload the materializer needs.
    3. New end-to-end constraint test (test_uk_target_references_constrain_a_frame_with_prepared_columns): all 13 active references compile from committed real feed rows (tests/fixtures/uk_target_reference_feed_rows.jsonl) into a TargetRegistry, and build_constraint_matrix compiles that registry against a household-weighted frame carrying one hand-prepared column per measure on the reference's entity table — asserting zero skipped targets, exact per-row achieved aggregates, and the fact values on the right-hand side. This is precisely the reviewer's requested guarantee, with the Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622 materializer's output stubbed as hand-prepared columns (which is its contract).
  • The reviewer's alternative (derive measure from value_variable, compile filters into the reference) is recorded as considered-and-rejected: it would duplicate the contract's binding payload into a second, str-only surface and mis-constrain every filtered count until the filters were fully mirrored — the opposite of the contract/references split this PR establishes.

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verified as a carrier: the test suite passes for me; the 186 targets audit clean for value-freedom — the only numeric value fields in the document are filter predicates (the documented carve-out), no target values anywhere at any nesting depth; and the registry-parity accounting closes (609 mapped + 42 signed exclusions = 651 rows at the pinned ref). Since the substantive review of this artifact happened on chronicle#164 and this carries it verbatim per the #166 ruling, I have nothing to re-litigate — re-homing it as an untyped resource on the US fiscal_target_references.json pattern is exactly what the ruling asked for, and porting the chronicle loader's guarantees into consumer-side tests (value-free at depth, closed-world binding kinds, projection families carrying their assertion policy) means the protections survived the move rather than evaporating with the lane.

The note flagging that #622's resolver must honor ledger_selector.dimension_values is well-placed — that's the seam where a silent mismatch would otherwise hide. Ready from my side once the MYE sex-split facts PR lands on the chronicle side.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Authoring-run membership report (first activation, period 2023)

Feed: merged UK consumer facts of chronicle main post-#173 (98,392 facts across 8 sources; bundle years 2023–2026 deduped on aggregate_fact_key). Candidates: 186. Membership rule: a candidate is active iff compile_ledger_target_references compiles it fail-closed against the feed at period 2023 under identity, with geography_level: "country" appended to the activation row's selector.

active — 13
  • isc.private_school_students (≤2023: 1, overall: 2)
  • ons.household_composition.couple_3_plus_children_households (≤2023: 6, overall: 8)
  • ons.household_composition.couple_no_children_households (≤2023: 6, overall: 8)
  • ons.household_composition.couple_non_dependent_children_only_households (≤2023: 6, overall: 8)
  • ons.household_composition.couple_under_3_children_households (≤2023: 6, overall: 8)
  • ons.household_composition.lone_households_over_65 (≤2023: 6, overall: 8)
  • ons.household_composition.lone_households_under_65 (≤2023: 6, overall: 8)
  • ons.household_composition.lone_parent_dependent_children_households (≤2023: 6, overall: 8)
  • ons.household_composition.lone_parent_non_dependent_children_households (≤2023: 6, overall: 8)
  • ons.household_composition.multi_family_households (≤2023: 6, overall: 8)
  • ons.household_composition.unrelated_adult_households (≤2023: 6, overall: 8)
  • ons.public_sector_employment (≤2023: 1, overall: 2)
  • ons.savings_interest_income (≤2023: 6, overall: 8)
no_fact_at_or_before_2023 — 101
  • dwp.benefit_cap.capped_households (≤2023: 0, overall: 1)
  • dwp.esa_claimants (≤2023: 0, overall: 1)
  • dwp.esa_contrib_claimants (≤2023: 0, overall: 1)
  • dwp.esa_income_claimants (≤2023: 0, overall: 1)
  • dwp.jsa_claimants (≤2023: 0, overall: 1)
  • dwp.pip.daily_living_enhanced_claimants (≤2023: 0, overall: 1)
  • dwp.pip.daily_living_standard_claimants (≤2023: 0, overall: 1)
  • dwp.uc.households_children_1 (≤2023: 0, overall: 1)
  • dwp.uc.households_children_2 (≤2023: 0, overall: 1)
  • dwp.uc.households_children_3 (≤2023: 0, overall: 1)
  • dwp.uc.households_children_4 (≤2023: 0, overall: 1)
  • dwp.uc.households_children_5_or_more (≤2023: 0, overall: 1)
  • dwp.uc.households_couple_no_children (≤2023: 0, overall: 1)
  • dwp.uc.households_couple_with_children (≤2023: 0, overall: 1)
  • dwp.uc.households_single_no_children (≤2023: 0, overall: 1)
  • dwp.uc.households_single_with_children (≤2023: 0, overall: 1)
  • dwp.uc.payment_distribution_couple_no_children (≤2023: 0, overall: 28)
  • dwp.uc.payment_distribution_couple_with_children (≤2023: 0, overall: 28)
  • dwp.uc.payment_distribution_lone_parent (≤2023: 0, overall: 28)
  • dwp.uc.payment_distribution_single (≤2023: 0, overall: 28)
  • dwp.uc.scotland_households_child_under_1 (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_affected (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_claimant_pip (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_disabled_child_element (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_in_3_children_households (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_in_4_children_households (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_in_5_children_households (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_in_6_plus_children_households (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.children_in_affected_households (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_3_children (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_4_children (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_5_children (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_6_plus_children (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_affected (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_claimant_pip (≤2023: 0, overall: 1)
  • dwp.uc.two_child_limit.households_disabled_child_element (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.it_relief_additional_rate (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.it_relief_basic_rate (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.it_relief_higher_rate (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.nics_relief_employee (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.nics_relief_employer (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.users_above_cap (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.users_below_cap (≤2023: 0, overall: 1)
  • hmrc.salary_sacrifice.users_total (≤2023: 0, overall: 1)
  • obr.attendance_allowance (≤2023: 0, overall: 7)
  • obr.capital_gains_tax (≤2023: 0, overall: 7)
  • obr.carers_allowance (≤2023: 0, overall: 7)
  • obr.child_benefit (≤2023: 0, overall: 7)
  • obr.council_tax (≤2023: 0, overall: 7)
  • obr.council_tax_england (≤2023: 0, overall: 7)
  • obr.council_tax_scotland (≤2023: 0, overall: 7)
  • obr.council_tax_wales (≤2023: 0, overall: 7)
  • obr.domestic_rates (≤2023: 0, overall: 7)
  • obr.esa (≤2023: 0, overall: 7)
  • obr.fuel_duties (≤2023: 0, overall: 7)
  • obr.housing_benefit (≤2023: 0, overall: 7)
  • obr.income_tax (≤2023: 0, overall: 7)
  • obr.jobseekers_allowance (≤2023: 0, overall: 7)
  • obr.ni (≤2023: 0, overall: 7)
  • obr.ni_employee (≤2023: 0, overall: 7)
  • obr.ni_employer (≤2023: 0, overall: 7)
  • obr.ni_self_employed (≤2023: 0, overall: 7)
  • obr.pension_credit (≤2023: 0, overall: 7)
  • obr.pip (≤2023: 0, overall: 7)
  • obr.sdlt (≤2023: 0, overall: 7)
  • obr.state_pension (≤2023: 0, overall: 7)
  • obr.statutory_maternity_pay (≤2023: 0, overall: 7)
  • obr.tv_licence_fee (≤2023: 0, overall: 7)
  • obr.universal_credit_in_cap (≤2023: 0, overall: 7)
  • obr.universal_credit_outside_cap (≤2023: 0, overall: 7)
  • obr.vat (≤2023: 0, overall: 7)
  • obr.winter_fuel_allowance (≤2023: 0, overall: 7)
  • scotgov.council_tax_stock.band_a (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_b (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_c (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_d (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_e (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_f (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_g (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.band_h (≤2023: 0, overall: 1)
  • scotgov.council_tax_stock.total (≤2023: 0, overall: 1)
  • scotgov.scottish_child_payment_spending (≤2023: 0, overall: 3)
  • slc.borrowers.plan_2_above_threshold (≤2023: 0, overall: 6)
  • slc.borrowers.plan_2_liable (≤2023: 0, overall: 6)
  • slc.borrowers.plan_5_above_threshold (≤2023: 0, overall: 6)
  • slc.borrowers.plan_5_liable (≤2023: 0, overall: 6)
  • slc.repayments.devolved_total (≤2023: 0, overall: 3)
  • slc.repayments.england_plan_1 (≤2023: 0, overall: 1)
  • slc.repayments.england_plan_2 (≤2023: 0, overall: 2)
  • slc.repayments.england_plan_5 (≤2023: 0, overall: 2)
  • slc.repayments.england_postgraduate (≤2023: 0, overall: 2)
  • slc.repayments.england_total_higher_education (≤2023: 0, overall: 1)
  • voa.council_tax_stock.band_a (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_b (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_c (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_d (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_e (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_f (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_g (≤2023: 0, overall: 4)
  • voa.council_tax_stock.band_h (≤2023: 0, overall: 4)
  • voa.council_tax_stock.total (≤2023: 0, overall: 4)
multi_fact_sum_resolution_pending — 23
  • hmrc.cgt.gains_total (≤2023: 37, overall: 37)
  • hmrc.cgt.taxpayers_total (≤2023: 37, overall: 37)
  • hmrc.spi.dividend_income.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.dividend_income.count_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.employment_income.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.employment_income.count_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.private_pension_income.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.private_pension_income.count_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.property_income.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.property_income.count_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.self_employment_income.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.self_employment_income.count_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.state_pension.amount_by_total_income_band (≤2023: 13, overall: 13)
  • hmrc.spi.state_pension.count_by_total_income_band (≤2023: 13, overall: 13)
  • ons.land.corporate_land_value (≤2023: 29, overall: 30)
  • ons.land.household_land_value (≤2023: 29, overall: 30)
  • ons.land.land_value (≤2023: 29, overall: 30)
  • slc.support.adult_dependants_grant_recipients (≤2023: 11, overall: 12)
  • slc.support.adult_dependants_grant_spend (≤2023: 11, overall: 12)
  • slc.support.maintenance_loan_recipients (≤2023: 11, overall: 12)
  • slc.support.maintenance_loan_spend (≤2023: 11, overall: 12)
  • slc.support.parents_learning_allowance_recipients (≤2023: 11, overall: 12)
  • slc.support.parents_learning_allowance_spend (≤2023: 11, overall: 12)
geography_ambiguous_at_country_level — 49
  • ons.population.age_0_9_by_region (≤2023: 120, overall: 240)
  • ons.population.age_10_19_by_region (≤2023: 120, overall: 240)
  • ons.population.age_20_29_by_region (≤2023: 120, overall: 240)
  • ons.population.age_30_39_by_region (≤2023: 120, overall: 240)
  • ons.population.age_40_49_by_region (≤2023: 120, overall: 240)
  • ons.population.age_50_59_by_region (≤2023: 120, overall: 240)
  • ons.population.age_60_69_by_region (≤2023: 120, overall: 240)
  • ons.population.age_70_79_by_region (≤2023: 120, overall: 240)
  • ons.population.age_80_89_by_region (≤2023: 120, overall: 240)
  • ons.population.female_0_4 (≤2023: 20, overall: 40)
  • ons.population.female_10_14 (≤2023: 20, overall: 40)
  • ons.population.female_15_19 (≤2023: 20, overall: 40)
  • ons.population.female_20_24 (≤2023: 20, overall: 40)
  • ons.population.female_25_29 (≤2023: 20, overall: 40)
  • ons.population.female_30_34 (≤2023: 20, overall: 40)
  • ons.population.female_35_39 (≤2023: 20, overall: 40)
  • ons.population.female_40_44 (≤2023: 20, overall: 40)
  • ons.population.female_45_49 (≤2023: 20, overall: 40)
  • ons.population.female_50_54 (≤2023: 20, overall: 40)
  • ons.population.female_55_59 (≤2023: 20, overall: 40)
  • ons.population.female_5_9 (≤2023: 20, overall: 40)
  • ons.population.female_60_64 (≤2023: 20, overall: 40)
  • ons.population.female_65_69 (≤2023: 20, overall: 40)
  • ons.population.female_70_74 (≤2023: 20, overall: 40)
  • ons.population.female_75_79 (≤2023: 20, overall: 40)
  • ons.population.female_80_84 (≤2023: 20, overall: 40)
  • ons.population.female_85_90 (≤2023: 24, overall: 48)
  • ons.population.male_0_4 (≤2023: 20, overall: 40)
  • ons.population.male_10_14 (≤2023: 20, overall: 40)
  • ons.population.male_15_19 (≤2023: 20, overall: 40)
  • ons.population.male_20_24 (≤2023: 20, overall: 40)
  • ons.population.male_25_29 (≤2023: 20, overall: 40)
  • ons.population.male_30_34 (≤2023: 20, overall: 40)
  • ons.population.male_35_39 (≤2023: 20, overall: 40)
  • ons.population.male_40_44 (≤2023: 20, overall: 40)
  • ons.population.male_45_49 (≤2023: 20, overall: 40)
  • ons.population.male_50_54 (≤2023: 20, overall: 40)
  • ons.population.male_55_59 (≤2023: 20, overall: 40)
  • ons.population.male_5_9 (≤2023: 20, overall: 40)
  • ons.population.male_60_64 (≤2023: 20, overall: 40)
  • ons.population.male_65_69 (≤2023: 20, overall: 40)
  • ons.population.male_70_74 (≤2023: 20, overall: 40)
  • ons.population.male_75_79 (≤2023: 20, overall: 40)
  • ons.population.male_80_84 (≤2023: 20, overall: 40)
  • ons.population.male_85_90 (≤2023: 24, overall: 48)
  • ons.population.scotland_babies_under_1 (≤2023: 12, overall: 24)
  • ons.population.scotland_children_under_16 (≤2023: 192, overall: 384)
  • ons.population.scotland_households_3plus_children (≤2023: 1104, overall: 2208)
  • ons.population.uk_total (≤2023: 4, overall: 8)

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Adversarial review of this PR — findings and disposition

An adversarial Codex review of the branch diff was run locally on 2026-08-18 (challenge framing: question the design, not just hunt defects). Verdict: needs-attention, one high-severity finding.

The finding

[high] Active UK references use metric labels as measure columns and drop filters (uk/target_references.json)
The generated reference sets measure to the published metric name (e.g. obr/private_school_students) instead of the contract's model column/filter binding. TargetSpec.measure is later read as a column on the target entity table; the contract for the same target declares value_variable: person_count plus a filter. These references will either be skipped as missing slash-named columns during matrix compilation, or constrain the wrong aggregate — so the active subset can load and resolve facts without actually constraining UK calibration correctly. Recommendation: derive measure from the model-facing value_variable, carry the filters onto the reference, and add a test compiling all active references against a minimal frame asserting zero skipped targets.

Disposition: derivation rule upheld; the semantics it relied on are now explicit and proven

The review's factual reading of TargetSpec.measure is correct — but slash-named measures are the US pipeline's own doctrine, not a defect: target_spec_from_ledger_reference requires a measure precisely because "count-like facts must be represented as sums of prepared indicator columns", and the JCT materializer names its prepared household columns after their targets (hh[reform_spec.measure] = reform − baseline). The recommended alternative would mis-constrain exactly the filtered counts it worries about: value_variable: person_count with the filter dropped targets the whole population, and no raw model column exists for "households of type X" — the column must be prepared from the contract's binding payload (value_variable + filters/household_conditions + entity mapping). That preparation is the #622 UK materializer's contract, reached via metadata.contract_target_id; duplicating the binding payload onto the str-only reference metadata would create a second source of truth that drifts.

The legitimate gap the review exposed: this prepared-column contract was implicit and untested — a naive #622 consumer could have wired the registry straight to raw columns. Addressed in 3789525:

  1. Explicit contract — every reference's metadata now carries "measure_kind": "prepared_column", and the file description states that a measure names the column the Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622 materializer must prepare from the contract binding, never a raw model variable.
  2. Derivability pinned — the derivation-parity test now asserts every active reference's contract binding carries the value_variable payload the materializer needs.
  3. The requested guarantee, end to endtest_uk_target_references_constrain_a_frame_with_prepared_columns compiles all 13 active references from committed real feed rows (tests/fixtures/uk_target_reference_feed_rows.jsonl) into a TargetRegistry, then build_constraint_matrix compiles that registry against a household-weighted frame carrying one hand-prepared column per measure on the reference's entity table (person measures collapsing onto household weights) — asserting zero skipped targets, exact per-row achieved aggregates, and the resolved fact values on the right-hand side. The materializer's output is stubbed as hand-prepared columns, which is exactly its contract.

The considered-and-rejected record for the reviewer's alternative derivation is in the PR body's review-response section. Full suite + ruff green; CI running on the new head.

🤖 Generated with Claude Code

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Full review of the PR as it now stands — my earlier note covered only the carrier; the branch has since grown the selector-vocabulary extension, the 13-reference active subset, and the prepared-column contract from the adversarial disposition. I read all three, plus the membership report, and the 142 tests across the five touched suites pass for me.

The adversarial disposition was the right call, and the proof is genuinely end to end. The reviewer's factual reading of TargetSpec.measure was correct and its recommended fix would have been wrong — value_variable: person_count with the filter dropped constrains the whole population, and duplicating the binding payload onto the reference would mint a second source of truth. What you shipped instead is the honest resolution: measure_kind: "prepared_column" declared on every reference, contract_target_id as the join key to the binding payload the #622 materializer owns, and test_uk_target_references_constrain_a_frame_with_prepared_columns proving the whole active subset compiles into constraint rows with zero skipped targets and exact weighted aggregates. That test is the difference between a doctrine and a defended doctrine — a naive #622 consumer now breaks a named test instead of silently wiring raw columns.

The selector extension is fail-closed in the right direction, with one behavior worth documenting. _strict_scalar_equal's type(actual) is type(expected) guard is the correct paranoia — no 1 == True, no "5" == 5 — but its failure mode composes with the membership rule into silent inactivity: a dimension pinned as float 5.0 against a fact carrying int 5 doesn't error, it quietly drops the target from the active subset. The membership report makes today's 13-of-186 auditable, but the report is a PR comment, not an artifact. One sentence in the resource description (or a docstring note) saying "type-mismatched pins deactivate rather than fail — check the authoring-run report when a target is unexpectedly inactive" would tell the future debugger where to look. Same note applies to the list-valued selector arms: any()-semantics over candidates are right, but nothing currently rejects an empty list, which matches nothing and reads like it should match anything — a raise on empty would close that trap.

The membership rule itself is well-chosen. Active iff the reference compiles fail-closed against the real merged feed at period 2023, with the aggregate-fact keys pinned in tests (554,243 pupils, £86.04bn savings interest, each with its ledger.aggregate_fact.v2 key) — so the active subset isn't an opinion, it's a compilation result you can re-derive. Pinning two real compilations plus the full-subset fixture run covers both the value path and the breadth path. And 13 of 186 is the honest number for the current feed: the remaining 173 activate as the chronicle facts land, and the contract's registry-parity accounting (which my earlier note verified: 609 + 42 = 651) tells you exactly what's waiting.

The carrier half stands as previously verified — value-free at depth with only filter predicates carrying numbers, the chronicle loader's guarantees ported as consumer-side tests, the re-homing exactly per the #166 ruling. With the MYE sex-split facts landed on the chronicle side (#173 merged), the stated precondition is met. Ready from my side — the two documentation sentences above can ride this PR or a follow-up at your discretion.

@vahid-ahmadi
vahid-ahmadi self-requested a review August 19, 2026 13:49
juaristi22 and others added 9 commits August 19, 2026 17:06
chronicle#166 ruling (2026-08-13): selection contracts live in the
consumer; Chronicle is a facts-only layer. This re-homes the uk_national
contract from chronicle PR #164 (held in draft for the ruling) into
uk/uk_national_targets.json, following the US pattern - an untyped,
value-free resource the runtime interprets (fiscal_target_references
precedent), registered in the country package.

The contract carries the 186 value-free target declarations verbatim
(ledger_selector with dimension-value pins, measurement contracts,
policyengine bindings including the declarative counterfactual kinds,
per-target assertion policies) plus the full uk-data registry parity
accounting: 651 rows at pinned ref ebf733c = 609 mapped + 42 signed
exclusions. Values resolve only from a hash-pinned Chronicle consumer
artifact at build time; compilation to TargetSpec rows, the UK mapping
module, and the counterfactual metric providers are microcosm#622, and
the calibration seam is microcosm#623.

Tests pin the guarantees the chronicle profile loader used to enforce:
value-free and hook-free at every depth (filter-predicate carve-out),
closed-world binding kinds, selectors on every target, projection
families carrying allow_source_projection, unique metric names, and the
registry accounting closing exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e guard

The registry-parity accounting must name policyengine-uk-data (651 rows at
pinned ref ebf733c); same sha-locked historical-reference class as the
parity references already allowlisted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…guarantees

Registry parity gains unmapped_declarations (3 salary-sacrifice IT-relief
targets whose incumbent rows are suppressed by a 410-Gone CSV at the pin)
and accounting_notes (cross-level duplicate names; the 15 two-child-limit
rows are 2026-only, so the incumbent surface is 636 at 2025 vs 651 at
2026 — the #622 Fixture-B input). The parity test flips from subset to
exact closure. geography_levels canonicalized country-first on the 9 VOA
targets; issue citations normalized to their repo namespaces; tests now
pin target_id uniqueness, closed-world selector/binding vocabularies,
per-kind required fields, two-level reduce, and the CGT name equality
with UK_CGT_TARGET_SPECS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…antics

_fact_matches_selector learns dimension_values (strict typed scalar
equality, list membership, missing-dimension no-match — the chronicle#164
correction semantics), list-form dimensions selectors (exact
order-insensitive name-set match; [] means the dimensionless total row),
list-valued scalar keys (membership), and the chronicle spellings
record_set_id / groupby_dimension as aliases for the layout fields. The
mapping-form dimensions path is unchanged; unknown keys still raise. One
resolver now serves the US references, the Belgian file, and all three
re-homed UK contracts (#708).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First activation of the national contract: 13 references (ISC census,
the 10 ONS household-composition types, savings interest, public-sector
employment) — the targets that compile fail-closed against chronicle
facts at the 2023 base period under identity, with geography_level
country appended as the activation row's geography. Deferred classes are
enumerated for #622: 101 with no fact at or before 2023, 23 pending
multi-fact sum resolution, 49 ambiguous across country-level geographies.
Typed per the Belgian target_references.json convention; rich binding
payloads stay in the contract, referenced via metadata.contract_target_id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o end

Adversarial-review response: every active reference's metadata now carries
measure_kind: prepared_column and the file description states the contract
— the measure names the slash-named column the #622 UK materializer must
prepare from the contract binding payload (US prepared-indicator-column
doctrine; a raw value_variable measure would mis-constrain every filtered
count). New test compiles all 13 references from committed real feed rows
and builds the constraint matrix against a frame with one hand-prepared
column per measure: zero skipped targets, exact row aggregates, fact
values on the right-hand side. Derivation test now also asserts each
active reference's binding carries value_variable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The SPI-spine PR added a second test pinning the UK resource tuple; both
pins now carry uk_national_targets.json and target_references.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review response (vahid): an empty membership or dimension-pin list matches
nothing while reading like match-anything - both now raise at resolve time.
The strict-typed-equality footgun (a float 5.0 pin vs an int 5 fact
deactivates the target instead of erroring) is documented where the future
debugger will look: the _dimension_values_match docstring and the
target_references.json description both point at the authoring-run
membership report for unexpectedly inactive targets.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Both recommendations are in, on the head rebased over #717/#714/#716 (ce7aab12):

  1. Empty lists now raise instead of silently matching nothing — both arms: a list-valued scalar selector key (source_measure_id: []) and an empty dimension_values pin list each raise at resolve time with a message naming the trap ("matches nothing while reading like match-anything"). dimensions: [] is explicitly carved out in the docstring — it is the list-form exact name-set selector for the dimensionless total row, not a membership list. Tests: test_empty_membership_list_raises_instead_of_matching_nothing, test_empty_dimension_values_pin_list_raises.

  2. The silent-deactivation footgun is documented where the debugger will look_dimension_values_match's docstring and the target_references.json description both now state that a type-mismatched pin (float 5.0 vs int 5) deactivates rather than fails under the resolve-or-defer activation rule, and point at the authoring-run membership report as the first stop for an unexpectedly inactive target.

The rebase also carries #717's second UK resource-tuple pin (test_spi_spine_adds_no_country_package_resources), which now lists both contract resources.

🤖 Generated with Claude Code

A concurrent push folded main into this branch as a merge and included the
uv.lock regen for microcosm-frame's uk extra (stale since #717; CI syncs
without --locked so it stayed green). The merge's content is otherwise
identical to this rebased line, which supersedes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22
juaristi22 force-pushed the uk-national-contract branch from d8c8938 to 1ad49d5 Compare August 19, 2026 15:49
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Verified on the rebased head: both empty-list arms raise with the trap named in the message, the dimensions: [] carve-out is the right distinction (an exact name-set selector for the dimensionless total row is legitimately empty-adjacent in shape but not in meaning), and the silent-deactivation note now sits in both places a debugger would look. All three test suites pass for me. Nothing further — merge when ready.

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.

2 participants