Adopt the UK national calibration contract (re-homed from chronicle#164 per the #166 ruling) - #707
Adopt the UK national calibration contract (re-homed from chronicle#164 per the #166 ruling)#707juaristi22 wants to merge 10 commits into
Conversation
vahid-ahmadi
left a comment
There was a problem hiding this comment.
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.
7bf7cb5 to
d3916c1
Compare
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 active — 13
no_fact_at_or_before_2023 — 101
multi_fact_sum_resolution_pending — 23
geography_ambiguous_at_country_level — 49
|
Adversarial review of this PR — findings and dispositionAn 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
Disposition: derivation rule upheld; the semantics it relied on are now explicit and provenThe review's factual reading of 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:
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
left a comment
There was a problem hiding this comment.
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.
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>
|
Both recommendations are in, on the head rebased over #717/#714/#716 (
The rebase also carries #717's second UK resource-tuple pin ( 🤖 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>
d8c8938 to
1ad49d5
Compare
|
Verified on the rebased head: both empty-list arms raise with the trap named in the message, the |
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:
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 refebf733c. Every target is byte-identical to the chronicle#164 review state modulo thechronicle_selector→ledger_selectorkey rename and the signed deviations below.ledger_targets.py's selector vocabulary learns the chronicle#164 selector semantics (dimension_valuesstrict-typed pins, list-formdimensions, list membership,record_set_id/groupby_dimensionaliases), 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.uk/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 viametadata.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 inregistry_parity.unmapped_declarations: thehmrc.salary_sacrifice.it_relief_{basic,higher,additional}_ratedeclarations 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; thebuild_loss_matrix.py:376-394dispatch 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_notesrecord what the incumbent cannot self-report:ons/uk_population,ons/scotland_children_under_16,ons/scotland_babies_under_1) that name-only identity flattens;dwp/uc/two_child_limit/*rows are 2026-only, so the incumbent's effective surface is 636 at calibration year 2025 vs 651 at 2026 — the Fixture-B year input for Consume Ledger UK facts: uk_target_references, mapping module, and a compile-parity gate #622.Deviations from the chronicle#164 verbatim carry (fix-and-sign register)
geography_levelscanonicalized["country","region"]on thevoa.council_tax_stock.*targetslocal_targets.py)populace#622/#467→microcosm#…; bare#133/#134/#139/#159→chronicle#…)descriptiongains the selector-vocabulary noteledger_selectorkey now genuinely interoperates via this PR's resolver extensionregistry_paritygainsunmapped_declarations+accounting_notesDeliberately not fixed (carried for parity, adjudicate in #622): the 14
metric_name/source_nameprefix disagreements (uk-data registry keys carried verbatim), the all-186bindings.axiomstatus: "pending"stubs, and the one stringvalue_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 10ons.household_composition.*types (these exercise the newdimension_valuesmatching),ons.savings_interest_income,ons.public_sector_employment.Deferred, enumerated per target in the authoring run's membership report:
no_fact_at_or_before_2023multi_fact_sum_resolution_pendingresolution_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 spellingsgeography_ambiguous_at_country_levelcountry; per-targetgeography_idpinning is a #622 adjudicationThe 33
allow_source_projectiontargets 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_iduniqueness; closed-world selector keys and the 24-keybindings.policyenginevocabulary; per-kind required fields for the 3 counterfactual kinds; two-levelreduce(bindingany; predicateany/any_child_under/count/sum);dimension_valuesshape; CGT metric-name equality withUK_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-typeddimension_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-formdimensions.test_uk_target_references.py: typed load (count pinned), per-row derivation parity against the contract, all periods 2023, and a compile smoke throughcompile_ledger_target_referenceson two real feed rows (ISC pupils 554,243; ONS HAXV £86.04bn) asserting identity resolution and lineage metadata.uk_national_targets.jsonallowlisted intest_no_incumbent_data_package_references_in_live_treeunder the existing sha-locked historical-reference convention.Full suite +
ruff checkgreen 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_factsrelease-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..d3916c11isolate 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:
measurecarries 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:
measurenaming a prepared column is the US pipeline's own doctrine, not an accident:target_spec_from_ledger_referencerequires 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 rawvalue_variablemeasure would be wrong for exactly the filtered counts the reviewer cites:person_countwithout 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 bymetadata.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."measure_kind": "prepared_column", and the filedescriptionstates the prepared-column contract explicitly — no future consumer can mistake the measure for a raw model variable.value_variablepayload the materializer needs.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 aTargetRegistry, andbuild_constraint_matrixcompiles 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).measurefromvalue_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.