From 6a5734ee11f4cfdf5ba8635beafdfa64bb1a34aa Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 24 Aug 2026 17:39:50 +0200 Subject: [PATCH] Pin England-scoped SLC and scotgov targets to their publication geographies Adjudicates the 14 UK target references still held as no_fact_at_or_before_period after #754 and activates the 11 that are mechanical geography-pin bugs: scotgov.scottish_child_payment_spending (Scotland-stamped Scottish Budget facts vs a UK pin) and the ten slc.borrowers.*/slc.support.* ids (England-stamped publications whose contract bindings and model variables are England-scoped vs GB pins). Also treats ay-prefixed academic-year record-set tokens as period-bearing so the multi-year SLC series collapse to one period-invariant series and resolve to the latest fact at or before the target period. Active references 397 -> 408; the seven remaining holds (four obr council-tax ids, two dwp.pip claimant counts, slc.repayments.devolved_total) are deliberate and documented in the pin rule and tests. Co-Authored-By: Claude Fable 5 --- .../slc-scotgov-geography-pins.fixed.md | 1 + .../src/microcosm/build/ledger_targets.py | 2 +- .../build/uk/target_reference_membership.json | 201 ++++++++++----- .../microcosm/build/uk/target_references.json | 239 ++++++++++++++++++ .../tests/test_country_spec.py | 2 +- .../tests/test_ledger_targets.py | 66 +++++ .../tests/test_uk_national_calibration.py | 2 +- .../tests/test_uk_target_references.py | 76 ++++-- tools/generate_uk_target_references.py | 34 ++- 9 files changed, 526 insertions(+), 97 deletions(-) create mode 100644 changelog.d/slc-scotgov-geography-pins.fixed.md diff --git a/changelog.d/slc-scotgov-geography-pins.fixed.md b/changelog.d/slc-scotgov-geography-pins.fixed.md new file mode 100644 index 00000000..ac497515 --- /dev/null +++ b/changelog.d/slc-scotgov-geography-pins.fixed.md @@ -0,0 +1 @@ +Pin the England-scoped SLC borrower and student-support targets and the Scottish Government child-payment target to their publication geographies, and treat ay-prefixed academic-year record-set tokens as period-bearing so multi-year SLC series resolve to the latest fact at or before the target period. diff --git a/packages/microcosm-build/src/microcosm/build/ledger_targets.py b/packages/microcosm-build/src/microcosm/build/ledger_targets.py index 07c5a631..1d436e88 100644 --- a/packages/microcosm-build/src/microcosm/build/ledger_targets.py +++ b/packages/microcosm-build/src/microcosm/build/ledger_targets.py @@ -1222,7 +1222,7 @@ def _is_period_token(value: str) -> bool: normalized = value.lower().replace("-", "_") if normalized.startswith("month"): normalized = normalized[len("month") :] - if normalized[:2] in {"ty", "cy", "fy"}: + if normalized[:2] in {"ty", "cy", "fy", "ay"}: normalized = normalized[2:] parts = normalized.split("_", maxsplit=1) if len(parts) == 2 and all(part.isdigit() for part in parts): diff --git a/packages/microcosm-build/src/microcosm/build/uk/target_reference_membership.json b/packages/microcosm-build/src/microcosm/build/uk/target_reference_membership.json index 3bbfd605..ec507a41 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/target_reference_membership.json +++ b/packages/microcosm-build/src/microcosm/build/uk/target_reference_membership.json @@ -3,11 +3,11 @@ "target_period": 2025, "candidate_count": 417, "contract_target_count": 189, - "active_reference_count": 397, + "active_reference_count": 408, "status_counts": { - "active": 397, + "active": 408, "multi_fact": 1, - "no_fact_at_or_before_period": 18, + "no_fact_at_or_before_period": 7, "signed_excluded": 1 }, "geography_pins": { @@ -677,7 +677,7 @@ }, "scotgov.scottish_child_payment_spending": { "geography_level": "country", - "geography_id": "K02000001" + "geography_id": "S92000003" }, "ons.savings_interest_income": { "geography_level": "country", @@ -725,43 +725,43 @@ }, "slc.borrowers.plan_2_above_threshold": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.borrowers.plan_2_liable": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.borrowers.plan_5_above_threshold": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.borrowers.plan_5_liable": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.maintenance_loan_recipients": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.maintenance_loan_spend": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.parents_learning_allowance_recipients": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.parents_learning_allowance_spend": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.adult_dependants_grant_recipients": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "slc.support.adult_dependants_grant_spend": { "geography_level": "country", - "geography_id": "K03000001" + "geography_id": "E92000001" }, "dwp.uc.households": { "geography_level": "country", @@ -2617,6 +2617,36 @@ "from": "2024", "to": "2025" }, + { + "name": "slc.support.maintenance_loan_recipients", + "from": "2024", + "to": "2025" + }, + { + "name": "slc.support.maintenance_loan_spend", + "from": "2024", + "to": "2025" + }, + { + "name": "slc.support.parents_learning_allowance_recipients", + "from": "2024", + "to": "2025" + }, + { + "name": "slc.support.parents_learning_allowance_spend", + "from": "2024", + "to": "2025" + }, + { + "name": "slc.support.adult_dependants_grant_recipients", + "from": "2024", + "to": "2025" + }, + { + "name": "slc.support.adult_dependants_grant_spend", + "from": "2024", + "to": "2025" + }, { "name": "dwp.uc.households", "from": "2025-12", @@ -7371,14 +7401,17 @@ ] }, "scotgov.scottish_child_payment_spending": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "scotgov.scottish_child_payment_spending", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 3, + "matched_fact_count_at_or_before_period": 2, + "resolved_period": 2025, + "resolved_value": 471000000.0, + "resolved_fact_period": "2025", + "resolved_fact_key": "ledger_aggregate_fact_v2_20b43b0f8f61e448f28895cf" } ] }, @@ -7545,122 +7578,152 @@ ] }, "slc.borrowers.plan_2_above_threshold": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.borrowers.plan_2_above_threshold", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 6, + "matched_fact_count_at_or_before_period": 2, + "resolved_period": 2025, + "resolved_value": 4460000.0, + "resolved_fact_period": "2025", + "resolved_fact_key": "ledger_aggregate_fact_v2_02288c19caeae900fdaf1e0d" } ] }, "slc.borrowers.plan_2_liable": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.borrowers.plan_2_liable", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 6, + "matched_fact_count_at_or_before_period": 2, + "resolved_period": 2025, + "resolved_value": 9710000.0, + "resolved_fact_period": "2025", + "resolved_fact_key": "ledger_aggregate_fact_v2_37387933e73bcc8c1e04cf78" } ] }, "slc.borrowers.plan_5_above_threshold": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.borrowers.plan_5_above_threshold", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 6, + "matched_fact_count_at_or_before_period": 2, + "resolved_period": 2025, + "resolved_value": 35000.0, + "resolved_fact_period": "2025", + "resolved_fact_key": "ledger_aggregate_fact_v2_dd16b085f81ef7708a00511e" } ] }, "slc.borrowers.plan_5_liable": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.borrowers.plan_5_liable", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 6, + "matched_fact_count_at_or_before_period": 2, + "resolved_period": 2025, + "resolved_value": 230000.0, + "resolved_fact_period": "2025", + "resolved_fact_key": "ledger_aggregate_fact_v2_73ff136d820950ea404ddb6b" } ] }, "slc.support.maintenance_loan_recipients": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.maintenance_loan_recipients", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 1159761.0, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_f03bd7a10d0eee9917a3a199" } ] }, "slc.support.maintenance_loan_spend": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.maintenance_loan_spend", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 8591659718.080004, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_2900e25d7ee90f79fb3b87b9" } ] }, "slc.support.parents_learning_allowance_recipients": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.parents_learning_allowance_recipients", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 99645.0, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_d5df1c69846ab2fd3d0e50e1" } ] }, "slc.support.parents_learning_allowance_spend": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.parents_learning_allowance_spend", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 181421659.32, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_72028ce00b9569deec79d6e6" } ] }, "slc.support.adult_dependants_grant_recipients": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.adult_dependants_grant_recipients", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 18611.0, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_adc83b5d137ecb48fed77064" } ] }, "slc.support.adult_dependants_grant_spend": { - "status": "no_fact_at_or_before_period", + "status": "active", "candidates": [ { "name": "slc.support.adult_dependants_grant_spend", - "status": "no_fact_at_or_before_period", - "matched_fact_count_overall": 0, - "matched_fact_count_at_or_before_period": 0, - "error": "ledger_reference_compile_status_no_fact_at_or_before_period" + "status": "active", + "matched_fact_count_overall": 12, + "matched_fact_count_at_or_before_period": 12, + "resolved_period": 2025, + "resolved_value": 55364916.81, + "resolved_fact_period": "2024", + "resolved_fact_key": "ledger_aggregate_fact_v2_5d42660596c9088248e41e90" } ] }, diff --git a/packages/microcosm-build/src/microcosm/build/uk/target_references.json b/packages/microcosm-build/src/microcosm/build/uk/target_references.json index 2bc9a065..41a0d09a 100644 --- a/packages/microcosm-build/src/microcosm/build/uk/target_references.json +++ b/packages/microcosm-build/src/microcosm/build/uk/target_references.json @@ -9317,6 +9317,27 @@ "uprating_from_period": "2025-09", "uprating_to_period": 2025 }, + { + "name": "scotgov.scottish_child_payment_spending", + "ledger_selector": { + "source_name": "scotgov", + "source_concept": "scotgov.social_security_assistance_spending", + "dimension_values": { + "social_security_line": "scottish_child_payment" + }, + "geography_level": "country", + "geography_id": "S92000003" + }, + "entity": "person", + "measure": "sss/scottish_child_payment", + "family": "scotgov_social_security", + "period": 2025, + "metadata": { + "contract_target_id": "scotgov.scottish_child_payment_spending", + "measure_kind": "prepared_column" + }, + "assertion_policy": "allow_source_projection" + }, { "name": "ons.savings_interest_income", "ledger_selector": { @@ -9528,6 +9549,224 @@ "uprating_from_period": "2024", "uprating_to_period": 2025 }, + { + "name": "slc.borrowers.plan_2_above_threshold", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.student_loan_borrowers", + "dimension_values": { + "row_group": "Higher education total", + "repayment_plan": "plan_2", + "borrower_status": "above_repayment_threshold" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/plan_2_borrowers_above_threshold", + "family": "slc_borrowers", + "period": 2025, + "metadata": { + "contract_target_id": "slc.borrowers.plan_2_above_threshold", + "measure_kind": "prepared_column" + }, + "assertion_policy": "allow_source_projection" + }, + { + "name": "slc.borrowers.plan_2_liable", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.student_loan_borrowers", + "dimension_values": { + "row_group": "Higher education total", + "repayment_plan": "plan_2", + "borrower_status": "liable_to_repay" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/plan_2_borrowers_liable", + "family": "slc_borrowers", + "period": 2025, + "metadata": { + "contract_target_id": "slc.borrowers.plan_2_liable", + "measure_kind": "prepared_column" + }, + "assertion_policy": "allow_source_projection" + }, + { + "name": "slc.borrowers.plan_5_above_threshold", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.student_loan_borrowers", + "dimension_values": { + "row_group": "Higher education total", + "repayment_plan": "plan_5", + "borrower_status": "above_repayment_threshold" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/plan_5_borrowers_above_threshold", + "family": "slc_borrowers", + "period": 2025, + "metadata": { + "contract_target_id": "slc.borrowers.plan_5_above_threshold", + "measure_kind": "prepared_column" + }, + "assertion_policy": "allow_source_projection" + }, + { + "name": "slc.borrowers.plan_5_liable", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.student_loan_borrowers", + "dimension_values": { + "row_group": "Higher education total", + "repayment_plan": "plan_5", + "borrower_status": "liable_to_repay" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/plan_5_borrowers_liable", + "family": "slc_borrowers", + "period": 2025, + "metadata": { + "contract_target_id": "slc.borrowers.plan_5_liable", + "measure_kind": "prepared_column" + }, + "assertion_policy": "allow_source_projection" + }, + { + "name": "slc.support.maintenance_loan_recipients", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.maintenance_loan_recipients", + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/maintenance_loan_recipients", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.maintenance_loan_recipients", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, + { + "name": "slc.support.maintenance_loan_spend", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.maintenance_loan_amount_paid", + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/maintenance_loan_spend", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.maintenance_loan_spend", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, + { + "name": "slc.support.parents_learning_allowance_recipients", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.targeted_support_recipients", + "dimension_values": { + "student_support_product": "parents_learning_allowance" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/parents_learning_allowance_recipients", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.parents_learning_allowance_recipients", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, + { + "name": "slc.support.parents_learning_allowance_spend", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.targeted_support_amount_awarded", + "dimension_values": { + "student_support_product": "parents_learning_allowance" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/parents_learning_allowance_spend", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.parents_learning_allowance_spend", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, + { + "name": "slc.support.adult_dependants_grant_recipients", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.targeted_support_recipients", + "dimension_values": { + "student_support_product": "adult_dependants_grant" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/adult_dependants_grant_recipients", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.adult_dependants_grant_recipients", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, + { + "name": "slc.support.adult_dependants_grant_spend", + "ledger_selector": { + "source_name": "slc", + "source_concept": "slc.targeted_support_amount_awarded", + "dimension_values": { + "student_support_product": "adult_dependants_grant" + }, + "geography_level": "country", + "geography_id": "E92000001" + }, + "entity": "person", + "measure": "slc/adult_dependants_grant_spend", + "family": "slc_student_support", + "period": 2025, + "metadata": { + "contract_target_id": "slc.support.adult_dependants_grant_spend", + "measure_kind": "prepared_column" + }, + "uprating_from_period": "2024", + "uprating_to_period": 2025 + }, { "name": "dwp.uc.households", "ledger_selector": { diff --git a/packages/microcosm-build/tests/test_country_spec.py b/packages/microcosm-build/tests/test_country_spec.py index 6af8d265..53098596 100644 --- a/packages/microcosm-build/tests/test_country_spec.py +++ b/packages/microcosm-build/tests/test_country_spec.py @@ -393,7 +393,7 @@ def test_uk_target_references_accept_regenerated_contract_fields(self) -> None: spec = load_country_spec("uk") references = {reference.name: reference for reference in spec.target_references} - assert len(references) == 397 + assert len(references) == 408 assert references["obr.esa"].value_operation == "sum" assert references["dwp.uc.households"].value_operation == ( "calendar_year_average" diff --git a/packages/microcosm-build/tests/test_ledger_targets.py b/packages/microcosm-build/tests/test_ledger_targets.py index c42d92cb..13c608de 100644 --- a/packages/microcosm-build/tests/test_ledger_targets.py +++ b/packages/microcosm-build/tests/test_ledger_targets.py @@ -648,6 +648,72 @@ def test__given_period_bearing_groupby_value__then_latest_source_period_is_used( assert registry.specs[0].value == 65_900_000_000 +def test__given_academic_year_record_sets__then_latest_source_period_is_used() -> None: + """The SLC series key one record set per academic year (…ay2023, …ay2024). + + The ay-prefixed token must normalize away like ty/fy tokens do, so the + per-year record sets collapse to one period-invariant series and the + resolver can take the latest fact at or before the target period instead + of refusing with multi_fact. + """ + reference = LedgerTargetReference( + name="latest maintenance-loan recipients", + ledger_selector={ + "source_name": "slc", + "source_concept": "slc.maintenance_loan_recipients", + "geography_level": "country", + "geography_id": "E92000001", + }, + entity="person", + measure="slc/maintenance_loan_recipients", + period=2025, + family="slc_student_support", + ) + older = _consumer_fact_row( + aggregate_fact_key="ledger.aggregate_fact.v2:slc-2023", + legacy_fact_key="ledger.fact.v1:slc-2023", + value=1_100_000, + source={"source_name": "slc"}, + observed_measure={ + "source_name": "slc", + "source_measure_id": "maintenance_loan_recipients", + "source_concept": "slc.maintenance_loan_recipients", + "unit": "count", + }, + period={"type": "academic_year", "value": 2023}, + geography={"level": "country", "id": "E92000001"}, + entity={"name": "person"}, + dimensions={}, + layout={ + "record_set_id": "slc.support.table_3a.recipients.ay2023", + "groupby_dimension": "slc.support_line", + "groupby_value_id": "maintenance_loan", + "measure_id": "maintenance_loan_recipients", + }, + ) + newer = _consumer_fact_row( + **{ + **older, + "aggregate_fact_key": "ledger.aggregate_fact.v2:slc-2024", + "legacy_fact_key": "ledger.fact.v1:slc-2024", + "value": 1_159_761, + "period": {"type": "academic_year", "value": 2024}, + "layout": { + "record_set_id": "slc.support.table_3a.recipients.ay2024", + "groupby_dimension": "slc.support_line", + "groupby_value_id": "maintenance_loan", + "measure_id": "maintenance_loan_recipients", + }, + } + ) + + registry = compile_ledger_target_references( + [older, newer], [reference], country="uk" + ) + + assert registry.specs[0].value == 1_159_761 + + def test__given_selector_matches_future_year__then_latest_eligible_period_is_used() -> ( None ): diff --git a/packages/microcosm-build/tests/test_uk_national_calibration.py b/packages/microcosm-build/tests/test_uk_national_calibration.py index 49445d4f..f6eb7902 100644 --- a/packages/microcosm-build/tests/test_uk_national_calibration.py +++ b/packages/microcosm-build/tests/test_uk_national_calibration.py @@ -16,7 +16,7 @@ from microcosm.build.uk_runtime.national_frame import validate_uk_national_frame from microcosm.frame import EntitySchema, Frame, WeightKind, Weights -ACTIVE_REFERENCE_COUNT = 397 +ACTIVE_REFERENCE_COUNT = 408 def _uc_reference(**overrides) -> LedgerTargetReference: diff --git a/packages/microcosm-build/tests/test_uk_target_references.py b/packages/microcosm-build/tests/test_uk_target_references.py index 4bd50e1a..75869ba6 100644 --- a/packages/microcosm-build/tests/test_uk_target_references.py +++ b/packages/microcosm-build/tests/test_uk_target_references.py @@ -33,7 +33,7 @@ _value_operation_by_target_id, ) -ACTIVE_REFERENCE_COUNT = 397 +ACTIVE_REFERENCE_COUNT = 408 UK_DATA_REPO = "policyengine-" + "uk-data" FIXTURE_REFERENCE_NAMES = { @@ -184,27 +184,54 @@ def test_ons_age_total_targets_pin_exact_age_dimension_set() -> None: ]["dimensions"] == ["age"] -def test_scotgov_council_tax_stock_pins_are_scotland_and_the_rule_is_scoped() -> None: - """The CTAXBASE stock facts are stamped S92000003 in Chronicle. - - The substring pin rule sees no "scotland" in "scotgov" and used to pin the - nine stock targets to the UK, so they could never match. The fix is scoped - to the stock family: every other Scotland pin still comes from the - substring rule, and the unrelated scotgov child-payment target keeps the - UK pin it had. +def test_prefix_geography_pins_carry_scotgov_and_england_scoped_slc_families() -> None: + """Prefix pins carry the families the nation-substring rule cannot. + + Chronicle stamps Scottish Government facts S92000003, but the substring + rule sees no "scotland" in "scotgov" or "scottish_child_payment" and used + to pin the family to the UK, so it could never match. The SLC + borrower-plan forecasts and the student-support publication are England + publications (facts stamped E92000001) whose contract side is + England-scoped too — the borrower bindings filter country == ENGLAND + explicitly and the support model variables are England-gated by + construction — while the GB default pin could never match. + slc.repayments.devolved_total and the dwp.pip claimant counts keep the GB + pin and stay held: activating them needs contract redesign (per-nation + repayment rows; an England-and-Wales PIP binding), not a pin change. """ contract = _load_uk_resource("uk_national_targets.json") pins = _geography_pins(contract) - stock_ids = { + scotgov_ids = { + str(target["target_id"]) + for target in contract["targets"] + if str(target["target_id"]).startswith("scotgov.") + } + assert scotgov_ids == { + f"scotgov.council_tax_stock.band_{band}" for band in "abcdefgh" + } | { + "scotgov.council_tax_stock.total", + "scotgov.scottish_child_payment_spending", + } + assert {pins[target_id]["geography_id"] for target_id in scotgov_ids} == { + "S92000003" + } + + england_slc_ids = { str(target["target_id"]) for target in contract["targets"] - if str(target["target_id"]).startswith("scotgov.council_tax_stock.") + if str(target["target_id"]).startswith(("slc.borrowers.", "slc.support.")) } - assert stock_ids == {f"scotgov.council_tax_stock.band_{band}" for band in "abcdefgh"} | { - "scotgov.council_tax_stock.total" + assert len(england_slc_ids) == 10 + assert {pins[target_id]["geography_id"] for target_id in england_slc_ids} == { + "E92000001" } - assert {pins[target_id]["geography_id"] for target_id in stock_ids} == {"S92000003"} - assert pins["scotgov.scottish_child_payment_spending"]["geography_id"] == "K02000001" + assert pins["slc.repayments.devolved_total"]["geography_id"] == "K03000001" + assert ( + pins["dwp.pip.daily_living_standard_claimants"]["geography_id"] == "K03000001" + ) + assert ( + pins["dwp.pip.daily_living_enhanced_claimants"]["geography_id"] == "K03000001" + ) def haystack(target: dict) -> str: selector = target.get("ledger_selector") or {} @@ -228,12 +255,23 @@ def haystack(target: dict) -> str: for target_id, pin in pins.items() if pin["geography_id"] == "S92000003" } - assert scotland_pinned == stock_ids | substring_scotland + assert scotland_pinned == scotgov_ids | substring_scotland membership = _load_uk_resource("target_reference_membership.json") - for target_id in stock_ids: + for target_id in sorted(scotgov_ids): assert membership["geography_pins"][target_id]["geography_id"] == "S92000003" assert membership["targets"][target_id]["status"] == "active" + for target_id in sorted(england_slc_ids): + assert membership["geography_pins"][target_id]["geography_id"] == "E92000001" + assert membership["targets"][target_id]["status"] == "active" + for target_id in ( + "slc.repayments.devolved_total", + "dwp.pip.daily_living_standard_claimants", + "dwp.pip.daily_living_enhanced_claimants", + ): + assert ( + membership["targets"][target_id]["status"] == "no_fact_at_or_before_period" + ) def test_uk_target_reference_membership_report_is_packaged() -> None: @@ -242,9 +280,9 @@ def test_uk_target_reference_membership_report_is_packaged() -> None: assert membership["target_period"] == 2025 assert membership["active_reference_count"] == ACTIVE_REFERENCE_COUNT assert membership["status_counts"] == { - "active": 397, + "active": 408, "multi_fact": 1, - "no_fact_at_or_before_period": 18, + "no_fact_at_or_before_period": 7, "signed_excluded": 1, } assert membership["genuine_sum_residue"] diff --git a/tools/generate_uk_target_references.py b/tools/generate_uk_target_references.py index 8ccc4e3d..84b91fe0 100644 --- a/tools/generate_uk_target_references.py +++ b/tools/generate_uk_target_references.py @@ -162,15 +162,37 @@ def _geography_pins(contract: Mapping[str, Any]) -> dict[str, dict[str, str]]: SCOTGOV_COUNCIL_TAX_STOCK_PREFIX = "scotgov.council_tax_stock." +# Target-id prefixes whose geography pins cannot come from the nation-substring +# rule below. Each entry is justified by the layer that fixes the geography: +# the Chronicle fact stamp, the contract binding, or the model variable's own +# country gate. +TARGET_PREFIX_GEOGRAPHY_PINS: tuple[tuple[str, str], ...] = ( + # Scottish Government statistics are Scotland-scoped and Chronicle stamps + # them S92000003 (CTAXBASE chargeable dwellings, Scottish Budget + # social-security lines); the substring rule sees no "scotland" in + # "scotgov" or "scottish_child_payment" and would fall through to the UK + # pin, which never matches a Scotland-stamped fact. + ("scotgov.", "scotland"), + # The SLC borrower-plan forecasts Chronicle carries are England-scoped + # (facts stamped E92000001) and the contract bindings already filter + # country == ENGLAND explicitly, so the GB default could never match. + ("slc.borrowers.", "england"), + # The SLC student-support publication is England-scoped (facts stamped + # E92000001) and the bound model variables are England-gated by + # construction (maintenance_loan_in_england_system, + # parents_learning_allowance_eligible and adult_dependants_grant_eligible + # all require country == ENGLAND). slc.repayments.* stays with the + # substring rule: its england_* ids name their nation, and devolved_total + # needs a per-nation redesign before it can activate. + ("slc.support.", "england"), +) + def _geography_id_for_target(target: Mapping[str, Any]) -> str: target_id = str(target["target_id"]).lower() - if target_id.startswith(SCOTGOV_COUNCIL_TAX_STOCK_PREFIX): - # The Scottish Government CTAXBASE chargeable-dwelling facts are - # stamped S92000003 in Chronicle; the substring rule below sees no - # "scotland" in "scotgov" and would fall through to the UK pin, which - # never matches a Scotland-stamped fact. - return UK_GEOGRAPHY_IDS["scotland"] + for prefix, geography_key in TARGET_PREFIX_GEOGRAPHY_PINS: + if target_id.startswith(prefix): + return UK_GEOGRAPHY_IDS[geography_key] selector = target.get("ledger_selector") or {} concept = str(selector.get("source_concept", "")).lower() measure = str(selector.get("source_measure_id", "")).lower()