From 23d66ff3acf1b956eb8287680760001217111db5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 6 Jul 2026 15:06:12 -0400 Subject: [PATCH 1/3] docs: refresh CTR coverage-matrix row for PE-UK 2.89.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit council_tax_reduction gained a formula in PolicyEngine-UK 2.89.2 (PolicyEngine/policyengine-uk#1769 national core + #1771 Kingston, both merged), so matrix row 29 and note 6 — which said the variable "does NOT exist in PE-UK" and the surface was "PE-blocked" — were stale. Update them to the resolved state: the England pension-age path (gov.local_authorities.england.council_tax_reduction.pensioners: max-support 1, capital limit 16000, withdrawal 0.20) is parameter-identical to SI 2012/2885 and matches the rulespec England pension-age award penny-for-penny on a synthetic grid (verified, PE-UK 2.89.2). Record that CTR does not inherit the #1794 Housing Benefit earnings-disregard defect (its applicable-income path applies no earnings disregard) and note the uk-pe universe pins 2.89.2 where the running EFRS suites pin 2.88.56. Co-Authored-By: Claude Fable 5 --- docs/uk-coverage-matrix.md | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/uk-coverage-matrix.md b/docs/uk-coverage-matrix.md index ef350c71..c98d8110 100644 --- a/docs/uk-coverage-matrix.md +++ b/docs/uk-coverage-matrix.md @@ -71,7 +71,11 @@ EUROMOD Belgium scoper). (`uk-tax-benefits-efrs`, `uk-universal-credit-efrs`) on the same `enhanced_frs_2023_24` population at 2026, PE-UK `2.88.56` / core `3.26.11`. The six `uk-*-ukmod` suites target **UKMOD/EUROMOD, not PolicyEngine** — - excluded from this matrix except as noted. + excluded from this matrix except as noted. The `uk-pe` **conformance + universe** (`conformance/uk-pe.yaml`, #188) pins PolicyEngine-UK **2.89.2**; + where a program's PE behaviour differs between the running-suite pin + (2.88.56) and the universe pin (2.89.2) — notably Council Tax Reduction, + present only from ≥2.89.2 — the row cites the 2.89.2 behaviour. ## Status legend @@ -121,7 +125,7 @@ PE suite. | 26 | **Scottish Child Payment** | 0.5 (0.4) | 1 | `scottish_child_payment` (after `would_claim_scp`) | ENCODED end-to-end — ssi/2020/351 reg.18/20 → `scottish_child_payment_annual_amount` | **compared** — `uk-tax-benefits-efrs` (`scottish-child-payment-final`) — 0 mismatches | — | | 27 | **Tax-Free Childcare** | 0.4 (0.6) | 1 | `tax_free_childcare` (after `would_claim_tfc`) | **partial** — top-up rate + element + income ceiling (Childcare Payments Act 2014 ss.1/21 + uksi/2015/448 reg.15); no final cap/payment formula | **compared (params)** — 2 TFC params `parameter_value`; rate framing mismatch (`not_comparable`: statute 25% vs PE 20%) | encode final top-up + annual cap → Childcare Payments Act 2014; build TFC suite | | 28 | **Carer Support Payment (Scotland)** | 0.4 (0.1) | 1 (hybrid) | `carer_support_payment` (Scotland CA replacement, ≥2025) | ENCODED end-to-end — ssi/2023/302 reg.5/16 → `carer_support_payment_annual_amount` | **compared** — `uk-tax-benefits-efrs` (`carer-support-payment-final`) — 12 mismatches | — (resolve mismatches) | -| 29 | **Council Tax Reduction (current)** | n/a (PE var absent) | — | **`council_tax_reduction` does NOT exist in PE-UK TBS** (only legacy `council_tax_benefit`) | ENCODED — England pension-age (`council-tax-reduction.yaml`) + Kingston local scheme (`kingston…council-tax-reduction`, 19 rules) | **AMBIGUOUS** — `council_tax_reduction` is `direct_variable`-mapped, but the target PE variable is not in PE-UK; no CTR surface runs. Populace-adapter/reported fallback only | **encode-and-compare blocked on PE side**: PE-UK must add a `council_tax_reduction` variable (issue #1769 national core + council schemes) → LGFA 1992 s.13A + Sch.1A; council CTR schemes | +| 29 | **Council Tax Reduction (current)** | n/a (input-tested) | 1 (rules) | `council_tax_reduction` (household; = `council_tax_benefit` over benunit heads) — **now present in PE-UK ≥2.89.2** (#1769 national core + #1771 Kingston, both MERGED) | ENCODED — England pension-age (`council-tax-reduction.yaml`, SI 2012/2885 Sch.1) + Kingston working-age local scheme (`kingston…council-tax-reduction`, 19 rules) | **encoded, not compared** — `council_tax_reduction` is `direct_variable`-mapped (bridges/mappings/uk.yaml) to the now-present PE variable; no EFRS CTR surface in a running suite yet. Commensurable: PE's England-pensioner path (`england.council_tax_reduction.pensioners`: max_support 1, capital_limit £16,000, withdrawal 0.20) matches SI 2012/2885 penny-for-penny on a synthetic pension-age grid (verified, PE-UK 2.89.2). `council_tax` is a supplied input on **both** sides (PE `council_tax_reduction_maximum_eligible_liability` = `council_tax`; rulespec `council_tax_liability_for_year` input) | build EFRS `council-tax-reduction` surface projecting PE's applicable-amount/applicable-income/non-dep/`council_tax`/`savings` into the rulespec supplied inputs (the `uk_pension_credit` bridge pattern) → LGFA 1992 s.13A + Sch.1A; SI 2012/2885; council CTR schemes | | 30 | **JSA (income) / IS / WTC / CTC** | 0.0 (migrated) | 1/3 | `jsa_income`, `income_support`, `working_tax_credit`, `child_tax_credit` | IS/JSA: only capital-tariff (uksi/1987/1967 reg.53; 1996/207 reg.116). WTC/CTC: only element rate tables (uksi/2002/2005 Sch.2; 2002/2007 reg.7; 2024/247) — no taper/final | **compared (tariff/rates only)** — `income_support_tariff_income`, `jsa_income_tariff_income` `direct_variable`; WTC/CTC element params `parameter_value` (incl. a documented £1,015-vs-£1,010.22 PE bug) | low fiscal priority (all £0 in 2026, UC migration); encode taper if needed → JSA Regs 1996 / IS Regs 1987 / WTC & CTC Regs 2002 | | 31 | **LBTT (Scotland) / LTT (Wales)** | 0.2 / 0.1 | 1 | `land_and_buildings_transaction_tax`, `land_transaction_tax` | NOT ENCODED | — | encode → LTT(S)A 2013 / LTTA(W) 2017 | | 32 | **SDA** | 0.05 (0.01) | 3 | `sda` (reported passthrough × max rate) | **partial (rate)** — basic+age rates (Up-rating Order Sch.1); wrapper retired | **compared (rate)** — `sda` `direct_variable` (single rate) + `not_comparable` age-band rows; the `severe-disablement-allowance-final` bridge surface was removed (rulespec retired in 829ab1a) | (closed legacy benefit; low priority) → SSCBA 1992 s.68 | @@ -202,11 +206,20 @@ suite; "PE-blocked" = PolicyEngine-UK must add the variable first. compares UK-branch only. Encode Scottish (+Welsh) rate tables and add the Scottish comparison branch. → Scotland Act 2016; Welsh Rates (Wales Act 2014). -6. **Council Tax Reduction** (**PE-blocked** + encode) — rulespec has CTR - (England pension-age + Kingston); PE-UK has **no `council_tax_reduction` - variable** (only legacy `council_tax_benefit`). PolicyEngine must implement - CTR (issue #1769 national core + council schemes) before this can compare. → - LGFA 1992 s.13A + Sch.1A. +6. **Council Tax Reduction** (**unblocked** — build EFRS surface) — rulespec has + CTR (England pension-age SI 2012/2885 + Kingston working-age); PE-UK gained a + `council_tax_reduction` variable in **≥2.89.2** (#1769 national core + #1771 + Kingston, both MERGED), so the earlier PE-side block is resolved. The + `england.council_tax_reduction.pensioners` path is parameter-identical to + SI 2012/2885 (max_support 1, capital_limit £16,000, withdrawal 0.20) and + matches the rulespec England pension-age award penny-for-penny on a synthetic + grid (verified, PE-UK 2.89.2); it does **not** inherit the #1794 Housing + Benefit earnings-disregard defect (CTR's `council_tax_reduction_applicable_income` + applies no earnings disregard and never calls + `housing_benefit_applicable_income_disregard`). Remaining work is the EFRS + `council-tax-reduction` surface (bridging PE's applicable-amount/-income, + non-dep deductions, `council_tax`, and `savings` into the rulespec supplied + inputs) → LGFA 1992 s.13A + Sch.1A; SI 2012/2885. 7. **Attendance Allowance + Winter Fuel + Tax-Free Childcare finals** (encode+compare) — each has rate params compared but no eligibility/final From abdbb99e70d1dbb31252700c2c6fc6f1ba3298cc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 6 Jul 2026 15:06:23 -0400 Subject: [PATCH 2/3] oracles: add uk-council-tax-reduction comparison (rulespec vs PE-UK 2.89.2) Adds a case-grid comparison of the encoded England pension-age Council Tax Reduction scheme (rulespec-uk uk/policies/govuk/council-tax-reduction.yaml, SI 2012/2885 Schedule 1) against PolicyEngine-UK's council_tax_reduction (present from 2.89.2). council_tax liability is a supplied input on both engines, so the schemes are commensurable. scripts/generate_uk_council_tax_reduction.py runs a synthetic England pensioner-household grid through PolicyEngine-UK 2.89.2 live and the rulespec scheme through the axiom rules engine, bridging PE's own applicable amount / applicable income / non-dependant deductions / council_tax liability / savings into the rulespec supplied inputs (the uk-pension-credit bridge pattern), and writes a v2 report. comparisons/uk-council-tax-reduction.yaml registers it; the uk-council-tax-reduction-grid runner delegates to the generator and reuses the committed dashboard report on a runner without the PE-UK/engine stack (mirroring state-income-tax-liability-grid). Co-Authored-By: Claude Fable 5 --- comparisons/uk-council-tax-reduction.yaml | 45 ++ scripts/generate_uk_council_tax_reduction.py | 416 +++++++++++++++++++ scripts/run_comparison.py | 44 ++ 3 files changed, 505 insertions(+) create mode 100644 comparisons/uk-council-tax-reduction.yaml create mode 100644 scripts/generate_uk_council_tax_reduction.py diff --git a/comparisons/uk-council-tax-reduction.yaml b/comparisons/uk-council-tax-reduction.yaml new file mode 100644 index 00000000..1e19088b --- /dev/null +++ b/comparisons/uk-council-tax-reduction.yaml @@ -0,0 +1,45 @@ +# Council Tax Reduction — rulespec-uk England pension-age scheme vs PolicyEngine-UK +# on a synthetic pensioner-household grid. The Axiom side is the encoded England +# pension-age prescribed scheme (SI 2012/2885 Schedule 1), +# uk/policies/govuk/council-tax-reduction.yaml, evaluated through the axiom rules +# engine over PolicyEngine's own applicable amount / applicable income / +# non-dependant deductions / council_tax liability / savings (the uk-pension-credit +# bridge pattern). PolicyEngine gained a council_tax_reduction formula in 2.89.2 +# (PolicyEngine/policyengine-uk#1769 national core + #1771 Kingston, both merged). +# +# council_tax liability is a supplied input on both sides, so the schemes are +# commensurable and the grid matches to the penny (full award below the +# applicable amount, the 20 per cent taper interior, the 16000 capital screen, +# single and couple applicable amounts); the only residuals are sub-0.001 +# PolicyEngine float32 artifacts, inside tolerance. +# +# Run locally (needs a PolicyEngine-UK 2.89.2 env, a built axiom rules engine and +# the rulespec-uk checkout): +# uv run python scripts/run_comparison.py uk-council-tax-reduction + +name: uk-council-tax-reduction +title: UK Council Tax Reduction — Axiom vs PolicyEngine +description: >- + Compares the encoded England pension-age Council Tax Reduction scheme + (rulespec-uk uk/policies/govuk/council-tax-reduction.yaml, SI 2012/2885) against + PolicyEngine-UK council_tax_reduction (2.89.2) at the 2026 validation year on a + synthetic pensioner-household grid crossing the 20 per cent taper and the 16000 + capital limit. council_tax liability is a supplied input on both engines. +documentation: docs/uk-coverage-matrix.md + +runner: + type: uk-council-tax-reduction-grid + parameters: + period: "2026" + policyengine_uk_version: "2.89.2" + # Links this suite to the rulespec-uk monorepo in the affected map so a + # change to the encoded England pension-age scheme triggers a re-run. + rulespec_roots: + - $HOME/TheAxiomFoundation/rulespec-uk + +artifacts: + report_basename: axiom-policyengine-uk-council-tax-reduction + +dashboard: + filename: axiom-policyengine-uk-council-tax-reduction.json + suite: uk-council-tax-reduction diff --git a/scripts/generate_uk_council_tax_reduction.py b/scripts/generate_uk_council_tax_reduction.py new file mode 100644 index 00000000..d06f2ae7 --- /dev/null +++ b/scripts/generate_uk_council_tax_reduction.py @@ -0,0 +1,416 @@ +#!/usr/bin/env python3 +"""Council Tax Reduction case grid: rulespec-uk vs PolicyEngine-UK. + +Compares the encoded England pension-age Council Tax Reduction scheme +(rulespec-uk ``uk/policies/govuk/council-tax-reduction.yaml``, the SI 2012/2885 +prescribed requirements) against PolicyEngine-UK's ``council_tax_reduction`` on +a synthetic pensioner-household grid at the 2026 validation year. + +Both sides are commensurable because Council Tax Reduction takes the same +supplied inputs on each engine: + +* PolicyEngine-UK's ``council_tax_reduction_maximum_eligible_liability`` is + ``council_tax`` (a reported/projected household input), and its England + pension-age path (parameters + ``gov.local_authorities.england.council_tax_reduction.pensioners``: + maximum-support 1, capital limit £16,000, withdrawal rate 0.20) is + parameter-identical to SI 2012/2885 Schedule 1. +* The rulespec England pension-age award + ``council_tax_reduction_annual_amount`` is a closed-form function of five + supplied inputs — ``applicable_amount_for_year``, ``income_for_year``, + ``council_tax_liability_for_year``, ``non_dependant_deductions_for_year`` and + ``household_capital`` — the applicable amount / applicable income / non-dep + deductions PolicyEngine computes internally. + +The bridge mirrors the ``uk_pension_credit`` suite: for each synthetic +household this generator reads PolicyEngine's own computed applicable amount, +applicable income, non-dependant deductions, ``council_tax`` liability and +``savings``, and feeds those exact values as the rulespec supplied inputs, so +both engines test the identical means-test arithmetic. The Axiom side is +evaluated through the axiom rules engine (``AxiomRulesRunner``); its numbers are +the engine's, not a re-implementation. + +Because the two schemes are the same statute with the same inputs, the grid +matches to the penny across the full award surface — full award below the +applicable amount, the 20 per cent taper interior, the £16,000 capital screen, +and single/couple households. The only residuals are sub-£0.001 PolicyEngine +float32 artifacts, inside the £0.01 tolerance. + +Run locally (needs a PolicyEngine-UK 2.89.2 environment, a built axiom rules +engine, and the rulespec-uk checkout):: + + uv run python scripts/generate_uk_council_tax_reduction.py + +On a runner without those, the committed dashboard report stands (the runner in +run_comparison.py reuses it), exactly like the state income-tax grid. +""" + +from __future__ import annotations + +import json +import os +from dataclasses import dataclass +from datetime import date, timezone +from datetime import datetime +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +REPORTS = REPO_ROOT / "reports" +DASH_PUBLIC = REPO_ROOT / "dashboard" / "public" / "data" + +VALIDATION_YEAR = 2026 +POLICYENGINE_UK_VERSION = "2.89.2" + +#: rulespec-uk checkout (the encoded England pension-age scheme + companion +#: tests). Resolves from the environment, then the org-mirror default. +RULESPEC_UK = Path( + os.environ.get("RULESPEC_UK_CHECKOUT") + or os.path.expanduser("~/TheAxiomFoundation/rulespec-uk") +) + +#: Encoded program under comparison and its final award output. +CTR_PROGRAM = "uk/policies/govuk/council-tax-reduction.yaml" +CTR_BASE = "uk:policies/govuk/council-tax-reduction" +CTR_OUTPUT = f"{CTR_BASE}#council_tax_reduction_annual_amount" +CTR_CONCEPT = CTR_OUTPUT + +#: The five supplied inputs the England pension-age award reads (Judgment for +#: the immigration-control flag, Money for the rest). +_APPLICABLE_AMOUNT = f"{CTR_BASE}#applicable_amount_for_year" +_INCOME = f"{CTR_BASE}#income_for_year" +_COUNCIL_TAX = f"{CTR_BASE}#council_tax_liability_for_year" +_NON_DEP = f"{CTR_BASE}#non_dependant_deductions_for_year" +_CAPITAL = f"{CTR_BASE}#household_capital" +_IMMIGRATION = f"{CTR_BASE}#liable_person_subject_to_immigration_control" + +#: PolicyEngine-UK variables read for the bridge and the final compared output. +_PE_AWARD = "council_tax_reduction" +_PE_APPLICABLE_AMOUNT = "council_tax_reduction_applicable_amount" +_PE_APPLICABLE_INCOME = "council_tax_reduction_applicable_income" +_PE_NON_DEP = "council_tax_reduction_non_dep_deductions" +_PE_LIABILITY = "council_tax_reduction_maximum_eligible_liability" +_PE_CAPITAL = "savings" + +#: Comparison tolerance — matches the bridges/mappings/uk.yaml CTR entry and the +#: EFRS suites' pound tolerance. PolicyEngine-UK carries monetary variables in +#: float32, so a small relative tolerance absorbs the last-bit representation +#: difference on the taper-interior cases. +_TOLERANCE = 0.01 +_RELATIVE_TOLERANCE = 2e-7 + +UK_SCOPE = {"type": "country", "geoid": "UK"} + + +@dataclass(frozen=True) +class CTRCase: + """One synthetic England pensioner household on the CTR grid.""" + + case_id: str + head_age: int + state_pension: float + employment_income: float + savings: float + council_tax: float + couple: bool + scenario: str + + +def _grid() -> list[CTRCase]: + """England pension-age households exercising the SI 2012/2885 award surface. + + Covers: full award when income is at or below the applicable amount; the + 20 per cent taper interior (three award levels); the £16,000 capital screen + at and above the limit; a high council-tax liability; and single vs couple + applicable amounts. Every case is in England with a pension-age head, so the + supported scheme is the England pensioner prescribed scheme. + """ + + return [ + CTRCase("ctr-pa-single-full-below-aa", 70, 4000, 0, 0, 1800, False, + "full-award-income-below-applicable-amount"), + CTRCase("ctr-pa-single-full-income-at-aa", 70, 9000, 4000, 0, 1800, False, + "full-award-income-near-applicable-amount"), + CTRCase("ctr-pa-single-taper-interior-a", 70, 18312, 0, 0, 3000, False, + "taper-interior"), + CTRCase("ctr-pa-single-taper-interior-b", 70, 25164, 0, 0, 3000, False, + "taper-interior"), + CTRCase("ctr-pa-single-taper-interior-c", 70, 20000, 0, 0, 2500, False, + "taper-interior"), + CTRCase("ctr-pa-single-taper-to-zero", 70, 12000, 20000, 0, 1800, False, + "taper-exhausts-award"), + CTRCase("ctr-pa-single-capital-at-limit", 70, 4000, 0, 16000, 1800, False, + "capital-at-limit-eligible"), + CTRCase("ctr-pa-single-capital-over-limit", 70, 4000, 0, 20000, 1800, False, + "capital-over-limit-excluded"), + CTRCase("ctr-pa-single-high-council-tax", 72, 8000, 0, 0, 2600, False, + "full-award-high-liability"), + CTRCase("ctr-pa-couple-full-below-aa", 70, 6000, 0, 0, 2200, True, + "couple-full-award"), + CTRCase("ctr-pa-couple-taper-interior", 70, 24000, 0, 0, 2400, True, + "couple-taper-interior"), + ] + + +def _pe_situation(case: CTRCase) -> dict: + year = VALIDATION_YEAR + people = { + "person": { + "age": {year: case.head_age}, + "state_pension": {year: case.state_pension}, + "employment_income": {year: case.employment_income}, + } + } + members = ["person"] + if case.couple: + # A pension-age partner keeps the benefit unit on the pensioner scheme + # and lifts the applicable amount to the couple rate. + people["partner"] = {"age": {year: 68}, "state_pension": {year: 0}} + members = ["person", "partner"] + return { + "people": people, + "benunits": {"bu": {"members": members}}, + "households": { + "hh": { + "members": members, + "council_tax": {year: case.council_tax}, + "local_authority": {year: "KINGSTON_UPON_THAMES"}, + "region": {year: "LONDON"}, + "country": {year: "ENGLAND"}, + "savings": {year: case.savings}, + } + }, + } + + +def _policyengine_rows(cases: list[CTRCase]) -> dict[str, dict[str, float]]: + """PolicyEngine-UK award plus the internals bridged into the rulespec side.""" + + from policyengine_uk import Simulation + + year = VALIDATION_YEAR + rows: dict[str, dict[str, float]] = {} + for case in cases: + sim = Simulation(situation=_pe_situation(case)) + rows[case.case_id] = { + "award": float(sim.calculate(_PE_AWARD, year).sum()), + "applicable_amount": float( + sim.calculate(_PE_APPLICABLE_AMOUNT, year).sum() + ), + "applicable_income": float( + sim.calculate(_PE_APPLICABLE_INCOME, year).sum() + ), + "non_dep_deductions": float(sim.calculate(_PE_NON_DEP, year).sum()), + "council_tax_liability": float(sim.calculate(_PE_LIABILITY, year).sum()), + "capital": float(sim.calculate(_PE_CAPITAL, year).sum()), + } + return rows + + +def _axiom_awards( + cases: list[CTRCase], pe_rows: dict[str, dict[str, float]] +) -> dict[str, float]: + """Rulespec England pension-age award through the axiom rules engine. + + Each case feeds PolicyEngine's own applicable amount / applicable income / + non-dependant deductions / ``council_tax`` liability / ``savings`` as the + rulespec supplied inputs, so the two engines test the identical means-test + arithmetic (the ``uk_pension_credit`` bridge pattern). + """ + + import sys + + sys.path.insert(0, str(REPO_ROOT)) + from axiom_oracles.adapters.axiom.runner import AxiomRulesRunner + from axiom_oracles.core.case import Case + + program = RULESPEC_UK / CTR_PROGRAM + binary = os.environ.get("AXIOM_RULES_ENGINE_BINARY") + + axiom_cases: list[Case] = [] + for case in cases: + row = pe_rows[case.case_id] + axiom_cases.append( + Case( + case_id=case.case_id, + period=str(VALIDATION_YEAR), + metadata={ + "axiom_entity": "Household", + "axiom_entity_id": "household", + "axiom_inputs": { + _APPLICABLE_AMOUNT: row["applicable_amount"], + _INCOME: row["applicable_income"], + _COUNCIL_TAX: row["council_tax_liability"], + _NON_DEP: row["non_dep_deductions"], + _CAPITAL: row["capital"], + _IMMIGRATION: False, + }, + }, + outputs=(CTR_OUTPUT,), + ) + ) + + runner = AxiomRulesRunner( + program_path=program, + binary_path=binary, + default_entity="Household", + default_entity_id="household", + rulespec_repo_roots=(RULESPEC_UK,), + mode="explain", + ) + results = runner.run_cases(axiom_cases, [CTR_OUTPUT]) + awards: dict[str, float] = {} + for result in results: + if result.errors: + raise RuntimeError( + f"axiom rules engine failed for {result.household_id}: " + f"{result.errors}" + ) + awards[str(result.household_id)] = float(result.values[CTR_OUTPUT]) + return awards + + +def _match(left: float, right: float) -> bool: + diff = abs(left - right) + if diff <= _TOLERANCE: + return True + base = max(abs(left), abs(right)) + return base > 0 and diff / base <= _RELATIVE_TOLERANCE + + +def build_report( + cases: list[CTRCase], + pe_rows: dict[str, dict[str, float]], + axiom: dict[str, float], +) -> dict: + report_cases: list[dict] = [] + mismatches: list[dict] = [] + matches = 0 + for case in cases: + pe_award = pe_rows[case.case_id]["award"] + ax_award = axiom[case.case_id] + ok = _match(ax_award, pe_award) + matches += int(ok) + report_cases.append( + { + "case_id": case.case_id, + "concept": CTR_CONCEPT, + "scenario": case.scenario, + "couple": case.couple, + "council_tax_liability": pe_rows[case.case_id]["council_tax_liability"], + "applicable_amount": pe_rows[case.case_id]["applicable_amount"], + "applicable_income": pe_rows[case.case_id]["applicable_income"], + "household_capital": pe_rows[case.case_id]["capital"], + "axiom": ax_award, + "policyengine": pe_award, + "axiom_vs_policyengine": { + "difference": ax_award - pe_award, + "match": ok, + }, + } + ) + if not ok: + mismatches.append( + { + "case_id": case.case_id, + "concept": CTR_CONCEPT, + "kind": "amount_difference", + "engines": ["axiom", "policyengine"], + "left_engine": "axiom", + "right_engine": "policyengine", + "left": ax_award, + "right": pe_award, + "difference": ax_award - pe_award, + } + ) + n = len(cases) + mismatch_count = len(mismatches) + match_count = n - mismatch_count + match_rate = round(100.0 * matches / n, 6) if n else 100.0 + return { + "schema_version": "axiom.comparison_report.v2", + "suite": "uk-council-tax-reduction", + "concept": CTR_CONCEPT, + "population": "case-grid", + "validation_year": VALIDATION_YEAR, + "locales": ["UK"], + "scope": UK_SCOPE, + "engines": { + "axiom": CTR_OUTPUT, + "policyengine": _PE_AWARD, + }, + "tolerance": {"absolute": _TOLERANCE, "relative": _RELATIVE_TOLERANCE}, + "case_count": n, + "summary": { + "comparison_count": n, + "match_count": match_count, + "mismatch_count": mismatch_count, + "axiom_vs_policyengine_match_rate": match_rate, + "policyengine_matches": matches, + "weighted": { + "comparison_weight": n, + "match_weight": match_count, + "mismatch_weight": mismatch_count, + "match_rate": match_rate, + }, + "mismatches_by_concept": ( + [{"count": mismatch_count, "value": CTR_CONCEPT}] + if mismatch_count + else [] + ), + "mismatches_by_kind": ( + [{"count": mismatch_count, "value": "amount_difference"}] + if mismatch_count + else [] + ), + "mismatches_by_scenario": [], + "error_count": 0, + "errors_by_engine": [], + }, + "mismatches": mismatches, + "errors": [], + "cases": report_cases, + "provenance": { + "generated": datetime.now(timezone.utc).date().isoformat(), + "generator": "scripts/generate_uk_council_tax_reduction.py", + "axiom_engine": "axiom rules engine over " + f"rulespec-uk {CTR_PROGRAM}", + "policyengine_uk": POLICYENGINE_UK_VERSION, + "commensurability": ( + "PolicyEngine-UK England pension-age Council Tax Reduction " + "(gov.local_authorities.england.council_tax_reduction.pensioners: " + "max-support 1, capital limit 16000, withdrawal 0.20) vs the " + "SI 2012/2885 prescribed pension-age scheme; council_tax " + "liability is a supplied input on both sides. PolicyEngine's " + "computed applicable amount, applicable income and non-dependant " + "deductions are bridged into the rulespec supplied inputs." + ), + }, + } + + +def main() -> int: + cases = _grid() + pe_rows = _policyengine_rows(cases) + axiom = _axiom_awards(cases, pe_rows) + report = build_report(cases, pe_rows, axiom) + + REPORTS.mkdir(exist_ok=True) + DASH_PUBLIC.mkdir(parents=True, exist_ok=True) + basename = "axiom-policyengine-uk-council-tax-reduction" + stamp = date.today().isoformat() + (REPORTS / f"{basename}-{stamp}.json").write_text( + json.dumps(report, indent=2) + "\n" + ) + (DASH_PUBLIC / f"{basename}.json").write_text(json.dumps(report, indent=2) + "\n") + + summary = report["summary"] + print( + f"uk-council-tax-reduction: PE match " + f"{summary['axiom_vs_policyengine_match_rate']}% " + f"({summary['policyengine_matches']}/{report['case_count']} cases)" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/run_comparison.py b/scripts/run_comparison.py index 914edf97..74885e0a 100755 --- a/scripts/run_comparison.py +++ b/scripts/run_comparison.py @@ -699,6 +699,11 @@ def _build_run_provenance(config: dict, runner_type: str, output: Path) -> dict: "name": "policyengine", "policyengine_uk": params.get("policyengine_uk_version", "2.88.56"), } + elif runner_type == "uk-council-tax-reduction-grid": + oracle = { + "name": "policyengine", + "policyengine_uk": params.get("policyengine_uk_version", "2.89.2"), + } elif runner_type == "axiom-oracles-compare": engines = {str(params.get("left", "")), str(params.get("right", ""))} oracle = { @@ -1646,6 +1651,44 @@ def _run_state_income_tax_liability_grid(runner: dict, output: Path) -> None: output.write_text(source.read_text()) +def _run_uk_council_tax_reduction_grid(runner: dict, output: Path) -> None: + """Council Tax Reduction grid: rulespec-uk pension-age scheme vs PolicyEngine-UK. + + Delegates to scripts/generate_uk_council_tax_reduction.py, which runs a + synthetic England pensioner-household grid through PolicyEngine-UK 2.89.2 and + the encoded SI 2012/2885 England pension-age scheme (evaluated through the + axiom rules engine over PolicyEngine's own applicable amount / applicable + income / non-dependant deductions / council_tax liability / savings), then + writes one v2 report. On a runner without a PolicyEngine-UK environment or a + built axiom rules engine, the committed dashboard report is reused, exactly + like the state income-tax grid. + """ + del runner + generator = REPO_ROOT / "scripts" / "generate_uk_council_tax_reduction.py" + basename = "axiom-policyengine-uk-council-tax-reduction" + committed = REPO_ROOT / "dashboard" / "public" / "data" / f"{basename}.json" + cmd = [ + "uv", + "run", + "--python", + "3.13", + "--no-project", + "--with-editable", + str(REPO_ROOT), + "--with", + "policyengine-uk==2.89.2", + "python", + str(generator), + ] + try: + subprocess.run(cmd, check=True, cwd=REPO_ROOT) + except (subprocess.CalledProcessError, FileNotFoundError) as exc: + if not committed.exists(): + raise + print(f"CTR grid generation unavailable ({exc}); reusing {committed}.") + output.write_text(committed.read_text()) + + RUNNERS = { "axiom-encode-snap-ecps-compare": _run_axiom_encode_snap_ecps_compare, "axiom-encode-tax-ecps-compare": _run_axiom_encode_tax_ecps_compare, @@ -1653,6 +1696,7 @@ def _run_state_income_tax_liability_grid(runner: dict, output: Path) -> None: "axiom-oracles-compare": _run_axiom_oracles_compare, "euromod-synthetic-compare": _run_euromod_synthetic_compare, "state-income-tax-liability-grid": _run_state_income_tax_liability_grid, + "uk-council-tax-reduction-grid": _run_uk_council_tax_reduction_grid, } From a4c560baa5d1e91ac38c3259fac02308099648c0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 6 Jul 2026 15:06:35 -0400 Subject: [PATCH 3/3] conformance: cover council_tax_reduction on uk-pe (13/25 -> 14/25) The committed uk-council-tax-reduction report matches all 11 pensioner case-grid cases to the penny (the 20% taper interior, the 16000 capital screen, single and couple), residuals sub-0.001 from PolicyEngine float32, inside the 0.01 tolerance. Bind uk-pe:council_tax_reduction to the uk-council-tax-reduction suite so the scoreboard scores it covered, moving PolicyEngine-UK conformance coverage from 13/25 (52%) to 14/25 (56%) and lifting the ratchet floor to 14. Regenerate the scoreboard, per-policy detail, burn-down, freshness, and affected-map. Widen the covered-suite allowlist test to admit the case-grid CTR suite alongside the two EFRS suites. Co-Authored-By: Claude Fable 5 --- comparisons/affected_map.json | 8 + conformance/detail/uk-pe.json | 22 +- conformance/ratchet.yaml | 2 +- conformance/scoreboard.json | 7 +- conformance/uk-pe.yaml | 18 +- ...policyengine-uk-council-tax-reduction.json | 228 ++++++++++++++++++ .../public/data/conformance_detail_uk-pe.json | 22 +- .../public/data/conformance_scoreboard.json | 7 +- dashboard/public/data/freshness.json | 12 +- tests/test_conformance.py | 17 +- 10 files changed, 305 insertions(+), 38 deletions(-) create mode 100644 dashboard/public/data/axiom-policyengine-uk-council-tax-reduction.json diff --git a/comparisons/affected_map.json b/comparisons/affected_map.json index c2b8749e..2adfff7f 100644 --- a/comparisons/affected_map.json +++ b/comparisons/affected_map.json @@ -352,6 +352,14 @@ ], "source": "comparisons/uk-child-benefit-ukmod.yaml" }, + { + "suite": "uk-council-tax-reduction", + "report": "axiom-policyengine-uk-council-tax-reduction.json", + "repos": [ + "TheAxiomFoundation/rulespec-uk" + ], + "source": "comparisons/uk-council-tax-reduction.yaml" + }, { "suite": "uk-employer-nic", "report": "axiom-euromod-uk-employer-nic.json", diff --git a/conformance/detail/uk-pe.json b/conformance/detail/uk-pe.json index 6ce6484b..66ed11ab 100644 --- a/conformance/detail/uk-pe.json +++ b/conformance/detail/uk-pe.json @@ -152,18 +152,18 @@ "oracle_policy_name": "council_tax_reduction", "in_scope": true, "exclusion_reason": null, - "suite": null, - "covered": false, - "comparisons": null, - "matches": null, - "raw_match_rate": null, + "suite": "uk-council-tax-reduction", + "covered": true, + "comparisons": 11, + "matches": 11, + "raw_match_rate": 100.0, "explained_rate": null, - "unexplained": null, - "axiom_attributed_open": null, - "oracle_attributed": null, - "bridge_artifacts": null, - "note": "In-scope uncovered \u2014 council_tax_reduction now HAS a formula in PE-UK 2.89.2 (the matrix ambiguity at HEAD 45eb181c, where the variable was absent, is resolved on this pin); rulespec encodes England pension-age + council schemes but no CTR suite runs yet (matrix row 29 \u2192 LGFA 1992 s.13A + Sch.1A).", - "status": "uncovered" + "unexplained": 0, + "axiom_attributed_open": 0, + "oracle_attributed": 0, + "bridge_artifacts": 0, + "note": "Covered by the uk-council-tax-reduction case-grid suite. council_tax_reduction gained a formula in PE-UK 2.89.2 (#1769 national core + #1771 Kingston, both merged), resolving the matrix ambiguity at HEAD 45eb181c where the variable was absent. The suite compares PE-UK's England pension-age path (gov.local_authorities.england.council_tax_reduction.pensioners: max-support 1, capital limit 16000, withdrawal 0.20) against the rulespec SI 2012/2885 England pension-age scheme (uk/policies/govuk/council-tax-reduction.yaml) on a synthetic pensioner grid, bridging PE's own applicable amount / applicable income / non-dependant deductions / council_tax liability / savings into the rulespec supplied inputs (the uk-pension-credit bridge pattern). council_tax is a supplied input on both sides, so the schemes are commensurable; all 11 cases match to the penny (the taper interior, the capital screen, single and couple), residuals sub-0.001 (PE float32), inside tolerance. The England pension-age income test does not inherit the #1794 Housing Benefit earnings-disregard defect \u2014 council_tax_reduction_applicable_income applies no earnings disregard. matrix row 29 \u2192 LGFA 1992 s.13A + Sch.1A; SI 2012/2885.", + "status": "conformant" }, { "id": "uk-pe:dla", diff --git a/conformance/ratchet.yaml b/conformance/ratchet.yaml index 9aaed1cd..84e78c03 100644 --- a/conformance/ratchet.yaml +++ b/conformance/ratchet.yaml @@ -15,7 +15,7 @@ ratchets: axiom_attributed_open_max: 0 policies_in_scope: 27 - jurisdiction: uk-pe - covered_min: 13 + covered_min: 14 unexplained_max: 0 axiom_attributed_open_max: 0 policies_in_scope: 25 diff --git a/conformance/scoreboard.json b/conformance/scoreboard.json index bde8e21c..45fb5774 100644 --- a/conformance/scoreboard.json +++ b/conformance/scoreboard.json @@ -72,8 +72,8 @@ "jurisdiction": "uk-pe", "oracle": "policyengine-uk_2.89.2/uk", "policies_in_scope": 25, - "covered": 13, - "covered_pct": 52.0, + "covered": 14, + "covered_pct": 56.0, "excluded": 8, "excluded_by_reason": { "input_carrying": 8 @@ -87,7 +87,6 @@ "attendance_allowance", "business_rates", "capital_gains_tax", - "council_tax_reduction", "fuel_duty", "lbtt_ltt", "legacy_means_tested", @@ -98,7 +97,7 @@ "winter_fuel_allowance" ], "blocking_reasons": [ - "12 of 25 in-scope policies are not covered by a live suite: attendance_allowance, business_rates, capital_gains_tax, council_tax_reduction, fuel_duty, lbtt_ltt, legacy_means_tested, state_pension, tax_free_childcare, tv_licence, vat, winter_fuel_allowance" + "11 of 25 in-scope policies are not covered by a live suite: attendance_allowance, business_rates, capital_gains_tax, fuel_duty, lbtt_ltt, legacy_means_tested, state_pension, tax_free_childcare, tv_licence, vat, winter_fuel_allowance" ] } ] diff --git a/conformance/uk-pe.yaml b/conformance/uk-pe.yaml index 347a16a4..647201bc 100644 --- a/conformance/uk-pe.yaml +++ b/conformance/uk-pe.yaml @@ -103,10 +103,20 @@ policies: output_vars: - council_tax_reduction in_scope: true - suite: null - note: In-scope uncovered — council_tax_reduction now HAS a formula in PE-UK 2.89.2 (the matrix ambiguity - at HEAD 45eb181c, where the variable was absent, is resolved on this pin); rulespec encodes England - pension-age + council schemes but no CTR suite runs yet (matrix row 29 → LGFA 1992 s.13A + Sch.1A). + suite: uk-council-tax-reduction + note: 'Covered by the uk-council-tax-reduction case-grid suite. council_tax_reduction gained a formula + in PE-UK 2.89.2 (#1769 national core + #1771 Kingston, both merged), resolving the matrix ambiguity + at HEAD 45eb181c where the variable was absent. The suite compares PE-UK''s England pension-age path + (gov.local_authorities.england.council_tax_reduction.pensioners: max-support 1, capital limit 16000, + withdrawal 0.20) against the rulespec SI 2012/2885 England pension-age scheme + (uk/policies/govuk/council-tax-reduction.yaml) on a synthetic pensioner grid, bridging PE''s own + applicable amount / applicable income / non-dependant deductions / council_tax liability / savings + into the rulespec supplied inputs (the uk-pension-credit bridge pattern). council_tax is a supplied + input on both sides, so the schemes are commensurable; all 11 cases match to the penny (the taper + interior, the capital screen, single and couple), residuals sub-0.001 (PE float32), inside tolerance. + The England pension-age income test does not inherit the #1794 Housing Benefit earnings-disregard + defect — council_tax_reduction_applicable_income applies no earnings disregard. matrix row 29 → LGFA + 1992 s.13A + Sch.1A; SI 2012/2885.' - id: uk-pe:dla oracle_policy_name: dla oracle_policy_type: rate_from_category diff --git a/dashboard/public/data/axiom-policyengine-uk-council-tax-reduction.json b/dashboard/public/data/axiom-policyengine-uk-council-tax-reduction.json new file mode 100644 index 00000000..8ff67693 --- /dev/null +++ b/dashboard/public/data/axiom-policyengine-uk-council-tax-reduction.json @@ -0,0 +1,228 @@ +{ + "schema_version": "axiom.comparison_report.v2", + "suite": "uk-council-tax-reduction", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "population": "case-grid", + "validation_year": 2026, + "locales": [ + "UK" + ], + "scope": { + "type": "country", + "geoid": "UK" + }, + "engines": { + "axiom": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "policyengine": "council_tax_reduction" + }, + "tolerance": { + "absolute": 0.01, + "relative": 2e-07 + }, + "case_count": 11, + "summary": { + "comparison_count": 11, + "match_count": 11, + "mismatch_count": 0, + "axiom_vs_policyengine_match_rate": 100.0, + "policyengine_matches": 11, + "weighted": { + "comparison_weight": 11, + "match_weight": 11, + "mismatch_weight": 0, + "match_rate": 100.0 + }, + "mismatches_by_concept": [], + "mismatches_by_kind": [], + "mismatches_by_scenario": [], + "error_count": 0, + "errors_by_engine": [] + }, + "mismatches": [], + "errors": [], + "cases": [ + { + "case_id": "ctr-pa-single-full-below-aa", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "full-award-income-below-applicable-amount", + "couple": false, + "council_tax_liability": 1800.0, + "applicable_amount": 13312.0, + "applicable_income": 4000.0, + "household_capital": 0.0, + "axiom": 1800.0, + "policyengine": 1800.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-single-full-income-at-aa", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "full-award-income-near-applicable-amount", + "couple": false, + "council_tax_liability": 1800.0, + "applicable_amount": 13312.0, + "applicable_income": 12914.0, + "household_capital": 0.0, + "axiom": 1800.0, + "policyengine": 1800.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-single-taper-interior-a", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "taper-interior", + "couple": false, + "council_tax_liability": 3000.0, + "applicable_amount": 13312.0, + "applicable_income": 17163.599609375, + "household_capital": 0.0, + "axiom": 2229.680078125, + "policyengine": 2229.68017578125, + "axiom_vs_policyengine": { + "difference": -9.765625009094947e-05, + "match": true + } + }, + { + "case_id": "ctr-pa-single-taper-interior-b", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "taper-interior", + "couple": false, + "council_tax_liability": 3000.0, + "applicable_amount": 13312.0, + "applicable_income": 22645.19921875, + "household_capital": 0.0, + "axiom": 1133.36015625, + "policyengine": 1133.360107421875, + "axiom_vs_policyengine": { + "difference": 4.8828125045474735e-05, + "match": true + } + }, + { + "case_id": "ctr-pa-single-taper-interior-c", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "taper-interior", + "couple": false, + "council_tax_liability": 2500.0, + "applicable_amount": 13312.0, + "applicable_income": 18514.0, + "household_capital": 0.0, + "axiom": 1459.6, + "policyengine": 1459.5999755859375, + "axiom_vs_policyengine": { + "difference": 2.441406240905053e-05, + "match": true + } + }, + { + "case_id": "ctr-pa-single-taper-to-zero", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "taper-exhausts-award", + "couple": false, + "council_tax_liability": 1800.0, + "applicable_amount": 13312.0, + "applicable_income": 28114.0, + "household_capital": 0.0, + "axiom": 0.0, + "policyengine": 0.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-single-capital-at-limit", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "capital-at-limit-eligible", + "couple": false, + "council_tax_liability": 1800.0, + "applicable_amount": 13312.0, + "applicable_income": 4000.0, + "household_capital": 16000.0, + "axiom": 1800.0, + "policyengine": 1800.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-single-capital-over-limit", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "capital-over-limit-excluded", + "couple": false, + "council_tax_liability": 1800.0, + "applicable_amount": 13312.0, + "applicable_income": 4000.0, + "household_capital": 20000.0, + "axiom": 0.0, + "policyengine": 0.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-single-high-council-tax", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "full-award-high-liability", + "couple": false, + "council_tax_liability": 2600.0, + "applicable_amount": 13312.0, + "applicable_income": 8000.0, + "household_capital": 0.0, + "axiom": 2600.0, + "policyengine": 2600.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-couple-full-below-aa", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "couple-full-award", + "couple": true, + "council_tax_liability": 2200.0, + "applicable_amount": 19934.19921875, + "applicable_income": 6000.0, + "household_capital": 0.0, + "axiom": 2200.0, + "policyengine": 2200.0, + "axiom_vs_policyengine": { + "difference": 0.0, + "match": true + } + }, + { + "case_id": "ctr-pa-couple-taper-interior", + "concept": "uk:policies/govuk/council-tax-reduction#council_tax_reduction_annual_amount", + "scenario": "couple-taper-interior", + "couple": true, + "council_tax_liability": 2400.0, + "applicable_amount": 19934.19921875, + "applicable_income": 21966.0, + "household_capital": 0.0, + "axiom": 1993.63984375, + "policyengine": 1993.639892578125, + "axiom_vs_policyengine": { + "difference": -4.8828125045474735e-05, + "match": true + } + } + ], + "provenance": { + "generated": "2026-07-06", + "generator": "scripts/generate_uk_council_tax_reduction.py", + "axiom_engine": "axiom rules engine over rulespec-uk uk/policies/govuk/council-tax-reduction.yaml", + "policyengine_uk": "2.89.2", + "commensurability": "PolicyEngine-UK England pension-age Council Tax Reduction (gov.local_authorities.england.council_tax_reduction.pensioners: max-support 1, capital limit 16000, withdrawal 0.20) vs the SI 2012/2885 prescribed pension-age scheme; council_tax liability is a supplied input on both sides. PolicyEngine's computed applicable amount, applicable income and non-dependant deductions are bridged into the rulespec supplied inputs." + } +} diff --git a/dashboard/public/data/conformance_detail_uk-pe.json b/dashboard/public/data/conformance_detail_uk-pe.json index 6ce6484b..66ed11ab 100644 --- a/dashboard/public/data/conformance_detail_uk-pe.json +++ b/dashboard/public/data/conformance_detail_uk-pe.json @@ -152,18 +152,18 @@ "oracle_policy_name": "council_tax_reduction", "in_scope": true, "exclusion_reason": null, - "suite": null, - "covered": false, - "comparisons": null, - "matches": null, - "raw_match_rate": null, + "suite": "uk-council-tax-reduction", + "covered": true, + "comparisons": 11, + "matches": 11, + "raw_match_rate": 100.0, "explained_rate": null, - "unexplained": null, - "axiom_attributed_open": null, - "oracle_attributed": null, - "bridge_artifacts": null, - "note": "In-scope uncovered \u2014 council_tax_reduction now HAS a formula in PE-UK 2.89.2 (the matrix ambiguity at HEAD 45eb181c, where the variable was absent, is resolved on this pin); rulespec encodes England pension-age + council schemes but no CTR suite runs yet (matrix row 29 \u2192 LGFA 1992 s.13A + Sch.1A).", - "status": "uncovered" + "unexplained": 0, + "axiom_attributed_open": 0, + "oracle_attributed": 0, + "bridge_artifacts": 0, + "note": "Covered by the uk-council-tax-reduction case-grid suite. council_tax_reduction gained a formula in PE-UK 2.89.2 (#1769 national core + #1771 Kingston, both merged), resolving the matrix ambiguity at HEAD 45eb181c where the variable was absent. The suite compares PE-UK's England pension-age path (gov.local_authorities.england.council_tax_reduction.pensioners: max-support 1, capital limit 16000, withdrawal 0.20) against the rulespec SI 2012/2885 England pension-age scheme (uk/policies/govuk/council-tax-reduction.yaml) on a synthetic pensioner grid, bridging PE's own applicable amount / applicable income / non-dependant deductions / council_tax liability / savings into the rulespec supplied inputs (the uk-pension-credit bridge pattern). council_tax is a supplied input on both sides, so the schemes are commensurable; all 11 cases match to the penny (the taper interior, the capital screen, single and couple), residuals sub-0.001 (PE float32), inside tolerance. The England pension-age income test does not inherit the #1794 Housing Benefit earnings-disregard defect \u2014 council_tax_reduction_applicable_income applies no earnings disregard. matrix row 29 \u2192 LGFA 1992 s.13A + Sch.1A; SI 2012/2885.", + "status": "conformant" }, { "id": "uk-pe:dla", diff --git a/dashboard/public/data/conformance_scoreboard.json b/dashboard/public/data/conformance_scoreboard.json index bde8e21c..45fb5774 100644 --- a/dashboard/public/data/conformance_scoreboard.json +++ b/dashboard/public/data/conformance_scoreboard.json @@ -72,8 +72,8 @@ "jurisdiction": "uk-pe", "oracle": "policyengine-uk_2.89.2/uk", "policies_in_scope": 25, - "covered": 13, - "covered_pct": 52.0, + "covered": 14, + "covered_pct": 56.0, "excluded": 8, "excluded_by_reason": { "input_carrying": 8 @@ -87,7 +87,6 @@ "attendance_allowance", "business_rates", "capital_gains_tax", - "council_tax_reduction", "fuel_duty", "lbtt_ltt", "legacy_means_tested", @@ -98,7 +97,7 @@ "winter_fuel_allowance" ], "blocking_reasons": [ - "12 of 25 in-scope policies are not covered by a live suite: attendance_allowance, business_rates, capital_gains_tax, council_tax_reduction, fuel_duty, lbtt_ltt, legacy_means_tested, state_pension, tax_free_childcare, tv_licence, vat, winter_fuel_allowance" + "11 of 25 in-scope policies are not covered by a live suite: attendance_allowance, business_rates, capital_gains_tax, fuel_duty, lbtt_ltt, legacy_means_tested, state_pension, tax_free_childcare, tv_licence, vat, winter_fuel_allowance" ] } ] diff --git a/dashboard/public/data/freshness.json b/dashboard/public/data/freshness.json index db280397..56c2a708 100644 --- a/dashboard/public/data/freshness.json +++ b/dashboard/public/data/freshness.json @@ -2,7 +2,7 @@ "schema": "axiom_oracles.freshness.v1", "_comment": "Generated by scripts/check_vacuous_gate.py. TIME-INVARIANT report provenance timestamps + the rulespec repos each suite is affected by. Staleness (age > max_age_days) and freshness alarms are derived at render time from generated_at \u2014 never stored \u2014 so this file only changes when a report or the affected map changes.", "max_age_days": 14, - "generated_at": "2026-07-06T17:46:44Z", + "generated_at": "2026-07-06T19:02:25Z", "suites": [ { "suite": "ak-atap-standards", @@ -685,6 +685,16 @@ "ran_against": {}, "unstamped": false }, + { + "suite": "uk-council-tax-reduction", + "generated_at": null, + "run_kind": null, + "affected_repos": [ + "TheAxiomFoundation/rulespec-uk" + ], + "ran_against": {}, + "unstamped": true + }, { "suite": "uk-employer-nic", "generated_at": null, diff --git a/tests/test_conformance.py b/tests/test_conformance.py index fd086152..05d78f93 100644 --- a/tests/test_conformance.py +++ b/tests/test_conformance.py @@ -394,12 +394,23 @@ def test_uk_pe_council_tax_reduction_is_in_scope_on_this_pin(): def test_uk_pe_covered_programs_name_a_live_pe_suite(): - """Every covered program points at one of the two live PE-UK EFRS suites.""" + """Every covered program points at a live PolicyEngine-UK suite. + + Two are population EFRS suites (uk-tax-benefits-efrs, uk-universal-credit-efrs); + uk-council-tax-reduction is a synthetic pensioner case-grid comparing the + SI 2012/2885 England pension-age scheme against PE-UK's council_tax_reduction + (present from 2.89.2). + """ universe = parse_universe(CONFORMANCE_DIR / "uk-pe.yaml") + live_pe_suites = { + "uk-tax-benefits-efrs", + "uk-universal-credit-efrs", + "uk-council-tax-reduction", + } covered_suites = { p.suite for p in universe.in_scope() if p.suite is not None } - assert covered_suites <= {"uk-tax-benefits-efrs", "uk-universal-credit-efrs"} + assert covered_suites <= live_pe_suites # The task's named covered additions are all present and suite-bound. by_name = universe.by_name() for program in ( @@ -417,6 +428,8 @@ def test_uk_pe_covered_programs_name_a_live_pe_suite(): "uk-tax-benefits-efrs", "uk-universal-credit-efrs", }, program + # Council Tax Reduction is covered by its case-grid suite. + assert by_name["council_tax_reduction"].suite == "uk-council-tax-reduction" def test_serialize_is_stable_roundtrip():