From 5f1350f08f614c153e8f3bde48999060a938e8d0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:30:59 -0400 Subject: [PATCH 1/3] Scope diagnoses, notes and construction premises to their engine Comparisons attached ten policyengine-uk 2.89.2 review diagnoses and 2.89.2-specific construction notes to every row, whatever engine produced it. They are now scoped: on another engine a review diagnosis becomes a prior_diagnoses entry with status not_reverified, and an authored note is labelled as written against 2.89.2. Event reports and summaries for a variant state its execution and its unsupported years. Legacy outputs are byte-identical. diagnose_uk_event_models.py takes --expect-engine and --reverify: it reruns the ten measured candidates on the installed engine and records whether each defect persists, is resolved or is inconclusive, from the case's own observation. Every predicate returns persists on its founding 2.89.2 observation. On 2.125.1 three are resolved (CGT commencement, the Class 4 freeze, employer NICs above State Pension age); seven persist. audit_uk_event_premises.py records, per engine, what each executed world holds at every annual lookup and whether each frozen construction's premises hold. Between 2.89.2 and 2.125.1 only the AB2024 CGT annualisation changed (fiscal_year_blend). CONSTRUCTION_REVISIONS.md proposes a revision; running it waits on d1344. Co-Authored-By: Claude Opus 5.5 --- docs/uk_replay/CONSTRUCTION_REVISIONS.md | 119 + pipeline/audit_uk_event_premises.py | 330 +++ pipeline/compare_uk_event.py | 275 +- pipeline/diagnose_uk_event_models.py | 366 ++- results/uk/events/CONSTRUCTION_PREMISES.json | 2322 +++++++++++++++++ .../CONSTRUCTION_PREMISES.json | 2322 +++++++++++++++++ .../MODEL_DIAGNOSTICS.json | 593 +++++ tests/test_uk_event_model_diagnostics.py | 5 +- tests/test_uk_event_premises.py | 252 ++ tests/test_uk_event_version_scope.py | 246 ++ 10 files changed, 6750 insertions(+), 80 deletions(-) create mode 100644 docs/uk_replay/CONSTRUCTION_REVISIONS.md create mode 100644 pipeline/audit_uk_event_premises.py create mode 100644 results/uk/events/CONSTRUCTION_PREMISES.json create mode 100644 results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json create mode 100644 results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/MODEL_DIAGNOSTICS.json create mode 100644 tests/test_uk_event_premises.py create mode 100644 tests/test_uk_event_version_scope.py diff --git a/docs/uk_replay/CONSTRUCTION_REVISIONS.md b/docs/uk_replay/CONSTRUCTION_REVISIONS.md new file mode 100644 index 0000000..83bb565 --- /dev/null +++ b/docs/uk_replay/CONSTRUCTION_REVISIONS.md @@ -0,0 +1,119 @@ +# Construction revisions for new engines + +The five event registries freeze constructions authored against +policyengine-uk 2.89.2. When a certified variant runs them on another engine, +`pipeline/audit_uk_event_premises.py` records what each executed world holds +at every annual lookup and whether the construction's premises still hold. +This document reviews the premises that changed and proposes revisions. + +**Every revision below is proposed, not applied.** Running a revised +construction changes the experiment, which is a methodology call under cos +decision d1344. Until that is ruled, variant reruns execute the frozen +registries unchanged, and the release diff carries the premise changes as +construction flags. + +Inputs (both parameter-tree only; no dataset or simulation): + +* `results/uk/events/CONSTRUCTION_PREMISES.json`: policyengine-uk 2.89.2 + (policyengine 5.0.2, core 3.27.1), the engine the registries were authored + against. +* `results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json`: + policyengine-uk 2.125.1 (policyengine 6.2.6, core 3.33.0). + +Regenerate either with +`PYTHONPATH=. python -m pipeline.audit_uk_event_premises --all --expect-engine --output ` +in that engine's environment; `--check` compares with the committed bytes. + +## Summary for 2.89.2 → 2.125.1 + +Of the 16 executable constructions, 15 keep every premise: the two forward +deltas still change current law, every reversal still differs from current +law in an in-force year, and every authored construction re-derives the same +values from 2.125.1's parameters. That includes the counterfactuals derived +from live engine values: the SB2023 UC childcare caps, the SS2025 UC standard +allowance and the SS2025 UC health element. + +One premise changed: how 2.125.1 annualises the AB2024 Capital Gains Tax +rates. + +| Measure | Premise | 2.89.2 | 2.125.1 | +|---|---|---|---| +| `autumn_budget_2024__capital_gains_main_rates_and_reliefs` | `annual_value_matches_30_april_sample` | holds | fails | + +## AB2024 Capital Gains Tax main rates + +**What changed in the engine.** In 2.125.1, +`parameters/gov/hmrc/cgt/basic_rate.yaml` (and the higher and additional +rate files) date the 18%/24% rates from 30 October 2024 and set +`metadata.fiscal_year_blend: true`. `utils/parameters.convert_to_fiscal_year_parameters` +day-weights such parameters across the fiscal year (6 April–5 April, +`fiscal_year_average`) instead of sampling 30 April. 2.89.2's conversion +reads no such metadata and sampled 30 April; its raw schedule started the +higher rates on 6 April 2025. + +**What the frozen construction now measures.** The construction reverses the +announced rates to 10%/20% from 30 October 2024; the executor moves that to +the next 1 January annual lookup (`compute_uk_event.annual_reform`), so the +reversal applies from CY2025. + +| CY | Path | 2.89.2 current law | 2.125.1 current law | Reversal world (both) | +|---|---|---:|---:|---:| +| 2024 | `basic_rate` | 0.10 | 0.13463 | current law | +| 2024 | `higher_rate`, `additional_rate` | 0.20 | 0.21732 | current law | +| 2025 | `basic_rate` | 0.18 | 0.18 | 0.10 | +| 2025 | `higher_rate`, `additional_rate` | 0.24 | 0.24 | 0.20 | + +On 2.89.2 the CY2024 worlds were identical because current law itself had no +2024 increase; the registry records the FY2024–25 row as a documented annual +activation (`stage_uk_event.DOCUMENTED_ANNUAL_ACTIVATIONS`). On 2.125.1 current +law does carry a 2024 increase (0.1346 = 0.10 × 207/365 + 0.18 × 158/365), +but the reversal leaves it in both worlds. The frozen construction therefore +omits a part-year FY2024–25 effect that the new engine models. + +**Proposed revision.** Extend the reversal to CY2024 at the pre-announcement +rates (`basic_rate` 0.10, `higher_rate` and `additional_rate` 0.20 from +1 January 2024), keeping CY2025 onwards unchanged. On 2.125.1 that scores +the blended FY2024–25 increase; on 2.89.2 it would remain inert for CY2024 +because current law already holds those rates, so the change is safe for the +legacy record. Remove the measure from `DOCUMENTED_ANNUAL_ACTIVATIONS` only +for variants whose engine blends these paths. Proposed; running it changes +the construction and waits on d1344. + +## Notes that state 2.89.2 facts + +These are not construction changes; the comparison now labels them. +`compare_uk_event.construction_note` prints the authored note on another +engine as "Authored against policyengine-uk 2.89.2" and omits the +2.89.2-only corrections, and the review diagnoses become +`prior_diagnoses` with `status: not_reverified`. + +* AS2023 Class 2 abolition: the "£3.15 for 2023-24" statement is a 2.89.2 + fact; 2.125.1's processed 2023 flat rate is £3.45. CY2023 is outside the + 6.2.6 variant's data years, so no 6.2.6 result depends on it. +* AB2024 SDLT surcharge: "the pin still has the 3% surcharge scale" also holds + on 2.125.1 (rates 3/5/8/13/15% at every lookup), so the forward construction + is unchanged. +* AB2024 private-school VAT: the lever is still 0 in every year on 2.125.1, so + the forward construction is unchanged. + +## Diagnoses re-verified on 2.125.1 + +`pipeline/diagnose_uk_event_models.py --reverify` reruns the ten measured +candidates on the installed engine; results are in the variant namespace's +`MODEL_DIAGNOSTICS.json`. + +| Candidate | 2.125.1 status | +|---|---| +| `carers_allowance_earnings_test_absent` | persists | +| `cgt_main_rate_commencement` | resolved (the blend above) | +| `sdlt_additional_home_hike_absent` | persists | +| `private_school_vat_current_law_lever_zero` | persists | +| `dividend_band_threshold_lag` | persists | +| `class4_threshold_indexation_from_2027` | resolved (limits held at £12,570/£50,270 through 2028) | +| `uc_lcwra_protection_stops_at_2030` | persists | +| `employer_nics_state_pension_age_exemption` | resolved (employer NICs at age 70 equal those at 45) | +| `pension_taper_omits_employer_contributions` | persists | +| `annual_allowance_charge_single_marginal_rate` | persists | + +A status describes the case's own synthetic observation. It does not size a +national effect or explain a replay movement on its own. diff --git a/pipeline/audit_uk_event_premises.py b/pipeline/audit_uk_event_premises.py new file mode 100644 index 0000000..32ed51d --- /dev/null +++ b/pipeline/audit_uk_event_premises.py @@ -0,0 +1,330 @@ +"""Record whether each frozen event construction's premises hold on an engine. + +The five event registries freeze constructions authored against +policyengine-uk 2.89.2. Some rely on that engine's current law or parameter +processing: a forward delta assumes its lever is absent from current law, a +reversal assumes the measure is present, derived counterfactuals were +computed from 2.89.2's parameter values, and the notes assume every +government parameter is sampled at 30 April. A rerun on another engine runs +the same frozen dictionaries against a different current law. This audit +records, per engine, what the executed worlds hold at each annual lookup and +whether each premise still holds, so the release diff can show where the +experiment itself moved. It changes no construction; a failed premise is a +revision candidate (docs/uk_replay/CONSTRUCTION_REVISIONS.md). + +Parameter tree only: no dataset, no simulation. + + PYTHONPATH=. python -m pipeline.audit_uk_event_premises --all \\ + --expect-engine 2.125.1 \\ + --output results/uk/replays//CONSTRUCTION_PREMISES.json +""" + +from __future__ import annotations + +import argparse +import hashlib +import importlib.metadata +import inspect +import json +import math +import sys +from pathlib import Path +from typing import Any, Callable + +ROOT = Path(__file__).resolve().parent.parent +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from pipeline import build_uk_event_registry as builder # noqa: E402 +from pipeline import compute_uk_ab2025 as worlds # noqa: E402 +from pipeline import compute_uk_event as compute # noqa: E402 + +EVENTS_DIR = ROOT / "data" / "uk" / "events" +EVENTS = tuple(builder.EVENTS) +CHECKS = ( + "forward_delta_changes_current_law", + "reversal_differs_from_current_law", + "authored_construction_reproduces", + "annual_value_matches_30_april_sample", +) +FORWARD = ("forward_delta_on_certified_world",) +REVERSAL = ("reversal_on_certified_world",) + + +def scalar(value: Any) -> Any: + """A JSON scalar: numpy scalars unwrapped, infinities named, NaN null.""" + if hasattr(value, "item"): + value = value.item() + if isinstance(value, float): + if math.isnan(value): + return None + if math.isinf(value): + return "inf" if value > 0 else "-inf" + if value is None or isinstance(value, (bool, int, float, str)): + return value + return type(value).__name__ + + +class EngineView: + """The installed engine's parameters, as the replay sees them. + + ``current_law`` reads the processed tree that simulations use. + ``raw_30_april`` reads ``parameters.baseline``, the clone each engine's + ``process_parameters`` takes before uprating, backdating and fiscal-year + conversion (policyengine_uk/tax_benefit_system.py). ``annualisation`` + reports whether the engine's ``convert_to_fiscal_year_parameters`` reads + the ``fiscal_year_blend``/``preserve_calendar_dates`` metadata at all. + """ + + def __init__(self, system=None): + import policyengine_uk + from policyengine_uk.utils import parameters as parameter_utils + + self.system = system or policyengine_uk.CountryTaxBenefitSystem() + source = inspect.getsource(parameter_utils.convert_to_fiscal_year_parameters) + self.reads_blend = "fiscal_year_blend" in source + self.reads_calendar = "preserve_calendar_dates" in source + self.resolve = builder.engine_resolver(self.system) + + def _node(self, tree, path: str): + try: + return worlds._resolve_parameter(tree, path) + except (AttributeError, IndexError): + return None + + def current_law(self, path: str, date: str) -> Any: + node = self._node(self.system.parameters, path) + return None if node is None else scalar(node(date)) + + def raw_30_april(self, path: str, year: int) -> Any: + node = self._node(self.system.parameters.baseline, path) + return None if node is None else scalar(node(f"{year}-04-30")) + + def metadata(self, path: str) -> dict: + node = self._node(self.system.parameters, path) + return dict(getattr(node, "metadata", None) or {}) + + def annualisation(self, path: str) -> dict: + metadata = self.metadata(path) + return { + "fiscal_year_blend": bool(metadata.get("fiscal_year_blend")) + if self.reads_blend + else None, + "preserve_calendar_dates": bool(metadata.get("preserve_calendar_dates")) + if self.reads_calendar + else None, + } + + +def window_value(reform: dict | None, path: str, date: str) -> tuple[bool, Any]: + """The value an annual reform dictionary sets at a date, if it sets one.""" + for window, value in ((reform or {}).get(path) or {}).items(): + start, stop = window.split(".") + if start <= date <= stop: + return True, scalar(worlds.NO_LIMIT if value is None else value) + return False, None + + +def _check(holds: bool | None, detail: str, **extra) -> dict: + return {"holds": holds, "detail": detail, **extra} + + +def authored_differences(event: str, measure: dict, resolve: Callable) -> dict: + """Re-author the measure's construction with this engine's parameters.""" + try: + authored = builder.authored_construction(event, measure["title"], resolve) + except Exception as exc: # noqa: BLE001 - a construction that cannot be authored here + return _check( + None, f"the builder cannot author this construction here: {exc!r}" + ) + if authored is None: + return _check(None, "no authored construction for this title") + differences = {} + for field in ("pe_baseline_modifier", "pe_reform_delta"): + frozen, fresh = measure.get(field), authored.get(field) + if (frozen or None) != (fresh or None): + differences[field] = {"frozen": frozen, "authored_here": fresh} + if differences: + return _check( + False, + "re-authoring with this engine's parameters gives other values than the " + "frozen registry", + differences=differences, + ) + return _check( + True, "re-authoring with this engine's parameters reproduces the registry" + ) + + +def audit_measure( + event: str, measure: dict, world: dict, years: list[int], engine: EngineView +) -> dict: + baseline = compute.annual_reform(world["baseline_reform"]) or {} + reform = compute.annual_reform(world["reform_reform"]) or {} + paths = sorted(set(baseline) | set(reform)) + commences = measure.get("commences_fy") + first = int(commences[:4]) if commences else years[0] + ends = measure.get("effect_ends_fy") + last = int(ends[:4]) if ends else years[-1] + year_records = {} + for year in years: + date = f"{year}-01-01" + records = {} + for path in paths: + current = engine.current_law(path, date) + found_b, base = window_value(baseline, path, date) + found_r, ref = window_value(reform, path, date) + records[path] = { + "current_law": current, + "baseline_world": base if found_b else current, + "reform_world": ref if found_r else current, + "raw_30_april": engine.raw_30_april(path, year), + **engine.annualisation(path), + } + year_records[str(year)] = {"in_force": first <= year <= last, "paths": records} + in_force = [r for r in year_records.values() if r["in_force"]] + construction = world["construction"] + checks = {} + if construction in FORWARD: + moved = any( + p["reform_world"] != p["current_law"] + for r in in_force + for p in r["paths"].values() + ) + checks["forward_delta_changes_current_law"] = _check( + moved, + "the forward delta changes current law in an in-force year" + if moved + else "the forward delta equals current law in every in-force year: inert", + ) + else: + checks["forward_delta_changes_current_law"] = _check( + None, "not a forward delta" + ) + if construction in REVERSAL: + moved = any( + p["baseline_world"] != p["current_law"] + for r in in_force + for p in r["paths"].values() + ) + checks["reversal_differs_from_current_law"] = _check( + moved, + "the reversal differs from current law in an in-force year" + if moved + else "the reversal equals current law in every in-force year: inert", + ) + else: + checks["reversal_differs_from_current_law"] = _check(None, "not a reversal") + checks["authored_construction_reproduces"] = authored_differences( + event, measure, engine.resolve + ) + if not engine.reads_blend and not engine.reads_calendar: + checks["annual_value_matches_30_april_sample"] = _check( + True, "this engine samples every government parameter at 30 April" + ) + else: + other = sorted( + { + path + for r in in_force + for path, p in r["paths"].items() + if p["fiscal_year_blend"] or p["preserve_calendar_dates"] + } + ) + checks["annual_value_matches_30_april_sample"] = ( + _check( + False, + "these paths are annualised another way (day-weighted fiscal-year " + "blend or statutory calendar dates): " + ", ".join(other), + ) + if other + else _check(True, "every touched path is sampled at 30 April") + ) + return {"construction": construction, "checks": checks, "years": year_records} + + +def audit_event( + event: str, registry: dict, registry_sha256: str, engine: EngineView +) -> dict: + index = {measure["measure_key"]: measure for measure in registry["measures"]} + years = sorted(registry["calendar_years"]) + planned = worlds.computable(index, years=years) + return { + "registry_sha256": registry_sha256, + "measures": { + key: audit_measure(event, index[key], world, years, engine) + for key, world in sorted(planned.items()) + if "alias_of" not in world + }, + } + + +def audit( + events: list[str], engine: EngineView, *, events_dir: Path = EVENTS_DIR +) -> dict: + out = {} + for event in events: + payload = (events_dir / f"{event}_measures.json").read_bytes() + out[event] = audit_event( + event, json.loads(payload), hashlib.sha256(payload).hexdigest(), engine + ) + return { + "schema_version": 1, + "engine": { + "policyengine": _version("policyengine"), + "policyengine_uk": _version("policyengine-uk"), + "policyengine_core": _version("policyengine-core"), + }, + "description": ( + "Executed-world values at each annual lookup (1 January) and premise " + "checks for the frozen event constructions on this engine. A failed " + "check is a construction revision candidate, not a model verdict." + ), + "events": out, + } + + +def _version(name: str) -> str | None: + try: + return importlib.metadata.version(name) + except importlib.metadata.PackageNotFoundError: + return None + + +def canonical_bytes(value: Any) -> bytes: + return ( + json.dumps(value, indent=1, sort_keys=True, allow_nan=False) + "\n" + ).encode() + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + selection = parser.add_mutually_exclusive_group(required=True) + selection.add_argument("--events", nargs="+", choices=EVENTS) + selection.add_argument("--all", action="store_true") + parser.add_argument("--expect-engine", required=True) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--check", action="store_true") + args = parser.parse_args(argv) + installed = _version("policyengine-uk") + if installed != args.expect_engine: + parser.error( + f"policyengine-uk {installed} installed; expected {args.expect_engine}" + ) + payload = canonical_bytes( + audit(list(EVENTS) if args.all else args.events, EngineView()) + ) + if args.check: + if not args.output.is_file() or args.output.read_bytes() != payload: + print(f"{args.output} differs from a fresh audit") + return 1 + print(f"{args.output} matches a fresh audit") + return 0 + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_bytes(payload) + print(f"wrote {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/pipeline/compare_uk_event.py b/pipeline/compare_uk_event.py index c5bf710..cdf6085 100644 --- a/pipeline/compare_uk_event.py +++ b/pipeline/compare_uk_event.py @@ -77,10 +77,46 @@ "full_event_complete", "missing_measure_years", "inert_measure_years", + "variant_id", + "unsupported_years", + "unsupported_measure_years", + "supported_grid_size", + "full_supported_grid_complete", ) +# The review diagnoses and construction notes below were established on this +# engine; on any other engine they are prior observations, not findings. +LEGACY_ENGINE = "2.89.2" +REVIEW_DIAGNOSES_ESTABLISHED_ON = (LEGACY_ENGINE,) -def review_diagnosis(measure_key: str, year: int, tax_head: str = "") -> dict: +def run_identity(directory: Path) -> dict: + """The execution behind an event directory, from its RUN_MANIFEST.json. + + Legacy manifests carry no variant fields; a directory without a manifest + is treated as the legacy run, as before.""" + path = directory / "RUN_MANIFEST.json" + manifest = json.loads(path.read_bytes()) if path.exists() else {} + return { + "engine_version": manifest.get("engine_version", LEGACY_ENGINE), + "policyengine_version": manifest.get("policyengine_version"), + "data_bundle": manifest.get("data_bundle"), + "variant_id": manifest.get("variant_id"), + "data_years": manifest.get("data_years"), + "unsupported_years": manifest.get("unsupported_years", []), + "run_manifest": path if "variant_id" in manifest else None, + } + + +def is_variant(identity: dict | None) -> bool: + return bool(identity and identity.get("variant_id")) + + +def review_diagnosis( + measure_key: str, + year: int, + tax_head: str = "", + engine_version: str = LEGACY_ENGINE, +) -> dict: """Retain the review's source-level diagnoses without sizing national terms. These observations concern the certified 2.89.2 pin. An upstream issue or @@ -230,7 +266,7 @@ def review_diagnosis(measure_key: str, year: int, tax_head: str = "") -> dict: ] else: return {} - return { + diagnosis = { "class": "pe_gap" if issue else "review_diagnosis", "cause_class": cause, "evidence": evidence, @@ -239,12 +275,34 @@ def review_diagnosis(measure_key: str, year: int, tax_head: str = "") -> dict: "issues": [issue] if issue else [], "national_effect": "unsized", } + if engine_version in REVIEW_DIAGNOSES_ESTABLISHED_ON: + return diagnosis + return { + "prior_diagnoses": [ + { + **diagnosis, + "established_on": list(REVIEW_DIAGNOSES_ESTABLISHED_ON), + "status": "not_reverified", + } + ] + } + +def construction_note(measure: dict, engine_version: str = LEGACY_ENGINE) -> str: + """Correct descriptive notes without invalidating unchanged result receipts. -def construction_note(measure: dict) -> str: - """Correct descriptive notes without invalidating unchanged result receipts.""" + The notes state facts about the engine they were written against. On any + other engine the authored note is labelled as such, and the 2.89.2-only + corrections below are not repeated as facts about this run.""" key = measure["measure_key"] note = measure.get("note", "") + if engine_version != LEGACY_ENGINE: + if not note: + return "" + return ( + f"Authored against policyengine-uk {LEGACY_ENGINE}: {note} Engine " + "facts for this run are recorded in CONSTRUCTION_PREMISES.json." + ) if key == "autumn_budget_2024__sdlt_additional_dwelling_surcharge_2pp": return ( "The pin still has the 3% surcharge scale, so apply a forward 2pp " @@ -543,8 +601,13 @@ def describe_decomposition( def build_comparison_rows( - registry: dict, staged_rows: list[dict], *, artifact_root: Path = ROOT + registry: dict, + staged_rows: list[dict], + *, + artifact_root: Path = ROOT, + identity: dict | None = None, ) -> list[dict]: + engine_version = (identity or {}).get("engine_version", LEGACY_ENGINE) inventory = {} for measure in registry["measures"]: for source in measure["source_rows"]: @@ -705,7 +768,7 @@ def build_comparison_rows( "ratio_bin": bin_name, "status": row.get("status"), "reason": reason, - "construction_note": construction_note(measure), + "construction_note": construction_note(measure, engine_version), "missing_legs": measure.get("missing_legs", []), "head_variables": variables, "artifact": reference, @@ -717,6 +780,7 @@ def build_comparison_rows( measure["measure_key"], int(source["fy"][:4]), source.get("tax_head", ""), + engine_version, ), }, **describe_decomposition(gap, axes, components), @@ -769,14 +833,59 @@ def profile(rows: list[dict], field: str) -> list[tuple[str, dict[str, int]]]: ] +def variant_description(identity: dict) -> str: + """What a non-legacy run executed, stated only from its manifest and + this repository's own executor.""" + years = identity.get("data_years") or [] + span = f"{years[0]}–{years[-1]}" if years else "unrecorded" + return ( + f"PolicyEngine is policyengine-uk {identity['engine_version']} with " + f"policyengine {identity['policyengine_version']}, on certified data build " + f"{identity['data_bundle']}: one survey population whose data years are " + f"{span}, projected by the engine to calendar year Y. Reform dictionaries " + "use explicit 1 January windows for annual lookups " + "(compute_uk_event.annual_reform)." + ) + + +def unsupported_lines(identity: dict | None, rows: list[dict]) -> list[str]: + unsupported = [r for r in rows if r.get("status") == "unsupported_on_variant"] + years = sorted( + {row["year"] for row in (identity or {}).get("unsupported_years", [])} + ) + if not years: + return [] + return [ + ( + f"Unsupported on this release: calendar years {', '.join(map(str, years))} " + "lie outside its population's data years, so they were not computed. " + f"{len(unsupported)} source rows keep their OBR values without a " + "PolicyEngine counterpart." + ), + "", + ] + + def render_markdown( - registry: dict, rows: list[dict], replay_grid: dict | None = None + registry: dict, + rows: list[dict], + replay_grid: dict | None = None, + identity: dict | None = None, ) -> str: computed = [r for r in rows if r["pe_value_gbp"] is not None] + variant = is_variant(identity) lines = [ f"# {registry.get('event_name', registry['event_slug'])}: descriptive replay", "", ( + "Positive GBP means a gain to the Exchequer. " + + variant_description(identity) + + " These calendar population inputs proxy OBR FY Y–(Y+1). OBR's " + "announcement forecast and behavioural costings remain distinct from " + "this construction." + ) + if variant + else ( "Positive GBP means a gain to the Exchequer. PolicyEngine is the pinned " "certified 2023 population uprated to calendar year Y, with government " "baseline policy parameters annualized from the engine's 30 April snapshot. " @@ -802,9 +911,11 @@ def render_markdown( ), "", ] + lines += unsupported_lines(identity, rows) if ( replay_grid and not replay_grid.get("full_event_complete") + and not replay_grid.get("full_supported_grid_complete") and not (inert_count or replay_grid.get("inert_measure_years")) ): lines += [ @@ -887,8 +998,13 @@ def render_markdown( "", ( "Every row tags population_vintage: OBR used the forecast at its fiscal " - "event, whereas this bundle uses one 2023 population calibrated to later " - "targets. The other declared axes are behavioural_adjustment (static versus " + + ( + "event, whereas this release projects one later survey population. " + if variant + else "event, whereas this bundle uses one 2023 population calibrated to later " + "targets. " + ) + + "The other declared axes are behavioural_adjustment (static versus " "behavioural), baseline_vintage (certified versus announcement baseline), " "cy_proxies_fy and head_scope. Partial constructions also tag construction_scope." ), @@ -949,28 +1065,52 @@ def render_markdown( ) + " |" ) + engine_version = (identity or {}).get("engine_version", LEGACY_ENGINE) caveats = [ measure for measure in registry["measures"] - if construction_note(measure) + if construction_note(measure, engine_version) and measure.get("classification", measure.get("computability")) in ("expressible", "partial") ] if caveats: lines += ["", "## Construction caveats", ""] for measure in caveats: - lines.append(f"* `{measure['measure_key']}`: {construction_note(measure)}") + lines.append( + f"* `{measure['measure_key']}`: {construction_note(measure, engine_version)}" + ) diagnosed = {} + prior = {} for row in rows: diagnosis = row.get("diagnosis") or {} if diagnosis.get("cause_class"): diagnosed.setdefault(row["measure_key"], diagnosis) + for earlier in diagnosis.get("prior_diagnoses", []): + prior.setdefault(row["measure_key"], earlier) if diagnosed: lines += ["", "## Review diagnoses", ""] for key, diagnosis in diagnosed.items(): lines.append( f"* `{key}` — {diagnosis['cause_class']}: {diagnosis['evidence']}" ) + if prior: + lines += [ + "", + ( + f"## Prior diagnoses, established on policyengine-uk {LEGACY_ENGINE} " + f"and not re-verified on {engine_version}" + ), + "", + ( + "These review observations describe the earlier engine. They are kept " + "for investigation and are not findings about this run." + ), + "", + ] + for key, diagnosis in prior.items(): + lines.append( + f"* `{key}` — {diagnosis['cause_class']}: {diagnosis['evidence']}" + ) return "\n".join(lines) + "\n" @@ -994,7 +1134,11 @@ def _receipt_path(reference: Any, artifact_root: Path) -> Path: def _verify_staging( - registry_path: Path, staged_path: Path, *, artifact_root: Path + registry_path: Path, + staged_path: Path, + *, + artifact_root: Path, + identity: dict | None = None, ) -> tuple[dict, list[dict], Path]: registry = _json(registry_path) manifest_path = staged_path.parent / "STAGING_MANIFEST.json" @@ -1006,7 +1150,10 @@ def _verify_staging( if manifest.get("staged_sha256") != _sha256(staged_path): raise EventComparisonError("staged SHA-256 differs from staging manifest") rows = build_comparison_rows( - registry, load_jsonl(staged_path), artifact_root=artifact_root + registry, + load_jsonl(staged_path), + artifact_root=artifact_root, + identity=identity, ) if manifest.get("staged_rows") != len(rows): raise EventComparisonError("staging manifest row count differs") @@ -1048,8 +1195,9 @@ def write_comparison( ) output_dir = output_dir or artifact_root / "results" / "uk" / "events" / event staged_path = staged_path or output_dir / "STAGED.jsonl" + identity = run_identity(staged_path.parent) registry, rows, staging_manifest_path = _verify_staging( - registry_path, staged_path, artifact_root=artifact_root + registry_path, staged_path, artifact_root=artifact_root, identity=identity ) if registry.get("event_slug") != event: raise EventComparisonError("registry event differs from --event") @@ -1062,7 +1210,7 @@ def write_comparison( } payloads = { "COMPARISON.csv": render_csv(rows), - "COMPARISON.md": render_markdown(registry, rows, replay_grid), + "COMPARISON.md": render_markdown(registry, rows, replay_grid, identity), "COMPARISON.json": json.dumps(rows, indent=1, sort_keys=True, allow_nan=False) + "\n", } @@ -1079,6 +1227,16 @@ def write_comparison( ), "staging_manifest_sha256": _sha256(staging_manifest_path), "axes_sha256": _sha256(AXES_PATH), + **( + { + "run_manifest_path": _relative_receipt( + identity["run_manifest"], artifact_root + ), + "run_manifest_sha256": _sha256(identity["run_manifest"]), + } + if identity["run_manifest"] is not None + else {} + ), "source_rows": len(rows), "replay_grid": replay_grid, "outputs_sha256": { @@ -1098,15 +1256,27 @@ def render_summary( diagnostics: list[dict] | None = None, registries: dict[str, dict] | None = None, replay_grids: dict[str, dict] | None = None, + identity: dict | None = None, ) -> str: rows = [row for event_rows in events.values() for row in event_rows] computed = [r for r in rows if r["pe_value_gbp"] is not None] registries = registries or {} replay_grids = replay_grids or {} + variant = is_variant(identity) + engine_version = (identity or {}).get("engine_version", LEGACY_ENGINE) lines = [ "# Recent OBR fiscal-event replays", "", ( + f"These replays ran on certified variant `{identity['variant_id']}`. " + + variant_description(identity) + + " Registry years outside the population's data years are reported as " + "unsupported, not computed. A seeded registry does not establish a " + "completed numerical replay. Available comparisons use one source head " + "and fiscal year per row." + ) + if variant + else ( "The inventories use the pinned populace-uk-2023 bundle definition with " "policyengine-uk 2.89.2. A seeded registry does not establish a completed " "numerical replay. Available comparisons use one source head and fiscal " @@ -1145,6 +1315,12 @@ def render_summary( state = ( "Registered construction replay complete" if replay_grid.get("full_event_complete") + else ( + "Registered construction replay complete on supported years; " + "unsupported: " + + ", ".join(map(str, replay_grid.get("unsupported_years", []))) + ) + if replay_grid.get("full_supported_grid_complete") else ( "Numerical comparison available; replay grid incomplete " f"({replay_grid.get('computed_measure_years', 0)}/" @@ -1215,8 +1391,12 @@ def render_summary( "", ( "population_vintage tags every row: the fiscal event's OBR forecast differs " - "from the certified 2023 population and its later calibration targets. " - "behavioural_adjustment, baseline_vintage, cy_proxies_fy and head_scope " + + ( + "from this release's later survey population. " + if variant + else "from the certified 2023 population and its later calibration targets. " + ) + + "behavioural_adjustment, baseline_vintage, cy_proxies_fy and head_scope " "describe the other construction differences. construction_scope identifies " "partial measures. The pipeline does not adjust the population vintage." ), @@ -1259,13 +1439,20 @@ def render_summary( diagnostic = { **diagnostic, **review_diagnosis( - row["measure_key"], row["year"], row.get("tax_head", "") + row["measure_key"], row["year"], row.get("tax_head", ""), engine_version ), } + for earlier in diagnostic.get("prior_diagnoses", [])[:1]: + diagnostic = { + "cause_class": f"Prior ({LEGACY_ENGINE}), not re-verified: " + + earlier["cause_class"], + "evidence": earlier["evidence"], + } evidence = diagnostic.get("evidence", "Open: relevant axes unsized") command = ( f"PYTHONPATH=. .venv-replay/bin/python pipeline/compute_uk_event.py --event {row['event_slug']} " - f"--measures {row['measure_key']} --years {row['year']} --workers 1 " + + (f"--variant {identity['variant_id']} " if variant else "") + + f"--measures {row['measure_key']} --years {row['year']} --workers 1 " f"--output-dir .venv-replay-checks/reproductions/{row['event_slug']}/{row['measure_key']}_{row['year']}" ) lines.append( @@ -1283,7 +1470,12 @@ def render_summary( diagnostics = diagnostics or [] active_events = set(events) | set(registries) relevant = [d for d in diagnostics if set(d.get("events", [])) & active_events] - concrete = [d for d in relevant if d.get("class") == "pe_gap"] + # A re-verified candidate counts only while its defect persists. + concrete = [ + d + for d in relevant + if d.get("class") == "pe_gap" and d.get("status", "persists") == "persists" + ] lines += [ "", ( @@ -1302,16 +1494,25 @@ def render_summary( "candidates; it is not padded to ten." ), "", - "| Candidate | Class | Variables | Evidence | Minimal reproducer |", - "|---|---|---|---|---|", + "| Candidate | Class | Variables | Evidence | Minimal reproducer |" + if not variant + else "| Candidate | Class | Status on this engine | Variables | Evidence | Minimal reproducer |", + "|---|---|---|---|---|" if not variant else "|---|---|---|---|---|---|", ] for diagnostic in relevant: command = ( "PYTHONPATH=. .venv-replay/bin/python pipeline/diagnose_uk_event_models.py " + diagnostic["reproducer"] + + (f" --expect-engine {engine_version}" if variant else "") + ) + status = ( + f"{_md(diagnostic.get('status', 'not re-verified'))} " + f"(established on {_md(', '.join(diagnostic.get('established_on', [LEGACY_ENGINE])))}) | " + if variant + else "" ) lines.append( - f"| {_md(diagnostic['id'])} | {_md(diagnostic['class'])} | " + f"| {_md(diagnostic['id'])} | {_md(diagnostic['class'])} | {status}" f"{_md(', '.join(diagnostic['variables']))} | " f"{_md(diagnostic['evidence']['engine_file'])} (source SHA and measured values in MODEL_DIAGNOSTICS.json) | `{command}` |" ) @@ -1341,8 +1542,16 @@ def load_verified_comparison(path: Path, *, artifact_root: Path = ROOT) -> list[ if _sha256(receipt) != provenance.get(f"{label}_sha256"): raise EventComparisonError(f"comparison {label} SHA-256 is stale") paths[label] = receipt + identity = run_identity(paths["staged"].parent) + if identity["run_manifest"] is not None and ( + provenance.get("run_manifest_sha256") != _sha256(identity["run_manifest"]) + ): + raise EventComparisonError("comparison run-manifest SHA-256 is stale") registry, verified_rows, manifest_path = _verify_staging( - paths["registry"], paths["staged"], artifact_root=artifact_root + paths["registry"], + paths["staged"], + artifact_root=artifact_root, + identity=identity, ) if manifest_path != paths["staging_manifest"]: raise EventComparisonError("comparison staging manifest path differs") @@ -1387,10 +1596,26 @@ def write_summary(events_root: Path, *, artifact_root: Path = ROOT) -> None: (artifact_root / "data" / "uk" / "events").glob("*_measures.json") ) } + identities = { + json.dumps( + { + k: v + for k, v in run_identity(path.parent).items() + if k not in ("run_manifest", "unsupported_years") + }, + sort_keys=True, + ) + for path in sorted(events_root.glob("*/COMPARISON.json")) + } + if len(identities) > 1: + raise EventComparisonError("summary events come from different executions") + identity = json.loads(identities.pop()) if identities else None events_root.mkdir(parents=True, exist_ok=True) atomic_write_bytes( events_root / "SUMMARY.md", - render_summary(events, diagnostics, registries, replay_grids).encode(), + render_summary( + events, diagnostics, registries, replay_grids, identity + ).encode(), ) diff --git a/pipeline/diagnose_uk_event_models.py b/pipeline/diagnose_uk_event_models.py index eae1ccd..75ba821 100644 --- a/pipeline/diagnose_uk_event_models.py +++ b/pipeline/diagnose_uk_event_models.py @@ -4,6 +4,12 @@ specified fraction of a national OBR difference. No issue is filed. The synthetic checks use one or two people rather than loading or simulating the certified national population, and release each world before the next. + +Each candidate was established on one engine. ``--reverify`` reruns every +case on the installed engine (``--expect-engine`` names it exactly) and +records, per case, whether the observation still shows the defect +(``persists``), shows it gone (``resolved``) or cannot decide +(``inconclusive``). The predicates read only the case's own observation. """ from __future__ import annotations @@ -34,8 +40,17 @@ def resolve(system, path: str, date: str): return value.item() if hasattr(value, "item") else value -def _source(package_root: Path, relative: str) -> dict: - payload = (package_root / relative).read_bytes() +LEGACY_ENGINE = "2.89.2" +STATUSES = ("persists", "resolved", "inconclusive") + + +def _source(package_root: Path, relative: str, *, tolerant: bool = False) -> dict: + path = package_root / relative + if tolerant and not path.is_file(): + # A moved or deleted file on another engine is a fact to record, not a + # resolution of the defect. + return {"engine_file": relative, "missing": True} + payload = path.read_bytes() return { "engine_file": relative, "sha256": hashlib.sha256(payload).hexdigest(), @@ -43,7 +58,9 @@ def _source(package_root: Path, relative: str) -> dict: } -def _additional_model_checks(package_root: Path, simulation_type, case: str | None): +def _additional_model_checks( + package_root: Path, simulation_type, case: str | None, *, tolerant: bool = False +): """Run each small diagnostic sequentially, retaining no live prior world.""" specifications = [ ( @@ -134,9 +151,14 @@ def _additional_model_checks(package_root: Path, simulation_type, case: str | No } simulation = simulation_type(situation=situation) try: - observation = { - name: simulation.calculate(name, year).tolist() for name in variables - } + observation = {} + for name in variables: + try: + observation[name] = simulation.calculate(name, year).tolist() + except Exception: # noqa: BLE001 - recorded, decided inconclusive + if not tolerant: + raise + observation[name] = None finally: del simulation gc.collect() @@ -149,9 +171,10 @@ def _additional_model_checks(package_root: Path, simulation_type, case: str | No "observation": observation, "situation": situation, "year": year, - "evidence": _source(package_root, evidence_path), + "evidence": _source(package_root, evidence_path, tolerant=tolerant), "supporting_evidence": [ - _source(package_root, path) for path in supporting_paths + _source(package_root, path, tolerant=tolerant) + for path in supporting_paths ], "primary_source": { "url": primary_url, @@ -166,12 +189,23 @@ def _additional_model_checks(package_root: Path, simulation_type, case: str | No return results -def investigate(case: str | None = None) -> list[dict]: +def investigate( + case: str | None = None, + *, + expect_engine: str = LEGACY_ENGINE, + tolerant: bool = False, +) -> list[dict]: + """Run the cases on the installed engine, which must be ``expect_engine``. + + ``tolerant`` (re-verification on another engine) records a missing source + file, parameter path or variable instead of raising; the predicates then + report ``inconclusive``. The default reproduces the established run. + """ for key in ("HF_HUB_OFFLINE", "HF_DATASETS_OFFLINE", "TRANSFORMERS_OFFLINE"): os.environ[key] = "1" version = importlib.metadata.version("policyengine-uk") - if version != "2.89.2": - raise RuntimeError(f"requires policyengine-uk 2.89.2; found {version}") + if version != expect_engine: + raise RuntimeError(f"requires policyengine-uk {expect_engine}; found {version}") import policyengine_uk from policyengine_uk import CountryTaxBenefitSystem, Simulation @@ -179,7 +213,34 @@ def investigate(case: str | None = None) -> list[dict]: system = CountryTaxBenefitSystem() results = [] - ca_source = _source(package_root, "variables/gov/dwp/carers_allowance.py") + def source(relative): + return _source(package_root, relative, tolerant=tolerant) + + def value(path, date): + try: + return resolve(system, path, date) + except (AttributeError, IndexError, KeyError): + if not tolerant: + raise + return None + + def calculated(simulation, name, year): + try: + return simulation.calculate(name, year).tolist() + except Exception: # noqa: BLE001 - recorded, decided inconclusive + if not tolerant: + raise + return None + + def formula_source(name, period): + try: + return inspect.getsource(system.variables[name].get_formula(period)) + except (KeyError, TypeError, OSError): + if not tolerant: + raise + return None + + ca_source = source("variables/gov/dwp/carers_allowance.py") # The two synthetic people differ only in earnings; both provide 35 hours # of care. Separate households avoid interactions in the simple formula. allowance = None @@ -210,7 +271,11 @@ def investigate(case: str | None = None) -> list[dict]: }, } ) - allowance = sim.calculate("carers_allowance", 2025).tolist() + try: + allowance = sim.calculate("carers_allowance", 2025).tolist() + except Exception: # noqa: BLE001 - recorded, decided inconclusive + if not tolerant: + raise del sim gc.collect() results.append( @@ -225,6 +290,14 @@ def investigate(case: str | None = None) -> list[dict]: "annual_carers_allowance_gbp": allowance, "earnings_limit_parameter_names": list( system.parameters.gov.dwp.carers_allowance.children + ) + if not tolerant + else sorted( + getattr( + getattr(system.parameters.gov.dwp, "carers_allowance", None), + "children", + {}, + ) ), }, "evidence": ca_source, @@ -235,7 +308,7 @@ def investigate(case: str | None = None) -> list[dict]: cgt = { date: { - name: resolve(system, f"gov.hmrc.cgt.{name}_rate", date) + name: value(f"gov.hmrc.cgt.{name}_rate", date) for name in ("basic", "higher", "additional") } for date in ("2024-10-29", "2024-10-30", "2025-04-06") @@ -247,9 +320,7 @@ def investigate(case: str | None = None) -> list[dict]: "events": ["autumn_budget_2024"], "variables": ["capital_gains_tax"], "observation": cgt, - "evidence": _source( - package_root, "parameters/gov/hmrc/cgt/basic_rate.yaml" - ), + "evidence": source("parameters/gov/hmrc/cgt/basic_rate.yaml"), "interpretation": "The harvested AB2024 title specifies 30 October 2024, while the raw main-rate schedule starts 6 April 2025. CountryTaxBenefitSystem annualizes government policy from the 30 April snapshot. The first higher processed year, 2025, is inferred from that conversion rule and the recorded April 2025 values. This records a missing part-year 2024 commencement, not an additional delay to 2026. The parameter description also warns that it is under active development. The national timing contribution remains unsized.", "reproducer": "--case cgt_main_rate_commencement", } @@ -257,7 +328,7 @@ def investigate(case: str | None = None) -> list[dict]: sdlt_path = "gov.hmrc.stamp_duty.residential.purchase.additional.rate" sdlt = { - date: [resolve(system, f"{sdlt_path}[{i}].rate", date) for i in range(5)] + date: [value(f"{sdlt_path}[{i}].rate", date) for i in range(5)] for date in ("2024-10-30", "2024-10-31", "2025-04-06", "2026-06-01") } results.append( @@ -270,8 +341,7 @@ def investigate(case: str | None = None) -> list[dict]: "stamp_duty_land_tax", ], "observation": sdlt, - "evidence": _source( - package_root, + "evidence": source( "parameters/gov/hmrc/stamp_duty/residential/purchase/additional/rate.yaml", ), "interpretation": "The source title raises HRAD from 3% to 5% on 31 October 2024. The pinned additional-home scale still has rates 3/5/8/13/15% at these dates. A forward +2pp construction models this missing announced change; it is not a reversal already present in certified law.", @@ -286,12 +356,10 @@ def investigate(case: str | None = None) -> list[dict]: "events": ["autumn_budget_2024"], "variables": ["private_school_vat"], "observation": { - date: resolve(system, "gov.contrib.labour.private_school_vat", date) + date: value("gov.contrib.labour.private_school_vat", date) for date in ("2025-01-01", "2025-06-01", "2026-06-01") }, - "evidence": _source( - package_root, "parameters/gov/contrib/labour/private_school_vat.yaml" - ), + "evidence": source("parameters/gov/contrib/labour/private_school_vat.yaml"), "interpretation": "The harvested AB2024 title applies 20% VAT to private-school education and boarding from January 2025. The pinned private-school VAT lever remains zero. The replay therefore executes a forward 20% construction; its imputed fees, attendance and omitted input-VAT recovery remain separate unsized scope limitations.", "reproducer": "--case private_school_vat_current_law_lever_zero", } @@ -299,17 +367,17 @@ def investigate(case: str | None = None) -> list[dict]: dividends = { date: { - "uk_higher_threshold": resolve( - system, "gov.hmrc.income_tax.rates.uk[1].threshold", date + "uk_higher_threshold": value( + "gov.hmrc.income_tax.rates.uk[1].threshold", date ), - "dividend_higher_threshold": resolve( - system, "gov.hmrc.income_tax.rates.dividends[1].threshold", date + "dividend_higher_threshold": value( + "gov.hmrc.income_tax.rates.dividends[1].threshold", date ), - "uk_additional_threshold": resolve( - system, "gov.hmrc.income_tax.rates.uk[2].threshold", date + "uk_additional_threshold": value( + "gov.hmrc.income_tax.rates.uk[2].threshold", date ), - "dividend_additional_threshold": resolve( - system, "gov.hmrc.income_tax.rates.dividends[2].threshold", date + "dividend_additional_threshold": value( + "gov.hmrc.income_tax.rates.dividends[2].threshold", date ), } for date in ("2024-06-01", "2025-06-01", "2026-06-01") @@ -325,9 +393,7 @@ def investigate(case: str | None = None) -> list[dict]: ], "variables": ["dividend_income_tax", "income_tax"], "observation": dividends, - "evidence": _source( - package_root, "parameters/gov/hmrc/income_tax/rates/dividends.yaml" - ), + "evidence": source("parameters/gov/hmrc/income_tax/rates/dividends.yaml"), "existing_issue": "https://github.com/PolicyEngine/policyengine-uk/issues/1822", "interpretation": "The existing mode-2 registry records the dividend thresholds lagging the main bands until April 2026. Their effect on a specific NICs costing's Income Tax interaction is unsized, so the national gap remains open.", "reproducer": "--case dividend_band_threshold_lag", @@ -336,9 +402,7 @@ def investigate(case: str | None = None) -> list[dict]: class4 = { date: { - name: resolve( - system, f"gov.hmrc.national_insurance.class_4.thresholds.{name}", date - ) + name: value(f"gov.hmrc.national_insurance.class_4.thresholds.{name}", date) for name in ("lower_profits_limit", "upper_profits_limit") } for date in ("2026-06-01", "2027-06-01", "2028-06-01") @@ -350,8 +414,7 @@ def investigate(case: str | None = None) -> list[dict]: "events": ["autumn_statement_2023", "spring_budget_2024"], "variables": ["ni_class_4"], "observation": class4, - "evidence": _source( - package_root, + "evidence": source( "parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml", ), "interpretation": "The existing AB2025 baseline integrity assessment states the prior-law Class 4 freeze runs to April 2028 while the pin resumes uprating in April 2027. This changes the marginal tax base in 2027; no fraction of a replay gap is attributed without a paired run.", @@ -367,7 +430,7 @@ def investigate(case: str | None = None) -> list[dict]: } ) - uc_source = _source(package_root, "scenarios/uc_reform.py") + uc_source = source("scenarios/uc_reform.py") uc_sim = None if case in (None, "uc_lcwra_protection_stops_at_2030"): uc_sim = Simulation( @@ -403,22 +466,19 @@ def investigate(case: str | None = None) -> list[dict]: "variables": ["uc_LCWRA_element"], "observation": { "monthly_disabled_element": { - date: resolve( - system, + date: value( "gov.dwp.universal_credit.elements.disabled.amount", date, ) for date in ("2025-06-01", "2026-06-01", "2030-06-01") }, "annual_synthetic_lcwra_element": { - str(year): uc_sim.calculate("uc_LCWRA_element", year).tolist() + str(year): calculated(uc_sim, "uc_LCWRA_element", year) if uc_sim is not None else None for year in (2029, 2030) }, - "formula_source": inspect.getsource( - system.variables["uc_LCWRA_element"].get_formula("2026") - ), + "formula_source": formula_source("uc_LCWRA_element", "2026"), }, "evidence": uc_source, "interpretation": "The default Simulation constructor executes a deterministic UC modifier. It protects seeded existing claimants for 2026–2029, so inspecting uc_LCWRA_element alone would incorrectly claim all claimants are halved. The modifier loops over range(2026,2030), leaving CY2030 outside that protection override. The synthetic observation isolates the cutoff; continued-protection policy scope requires review before calling it an encoding defect.", @@ -428,12 +488,197 @@ def investigate(case: str | None = None) -> list[dict]: if uc_sim is not None: del uc_sim gc.collect() - results.extend(_additional_model_checks(package_root, Simulation, case)) + results.extend( + _additional_model_checks(package_root, Simulation, case, tolerant=tolerant) + ) for result in results: result["engine_version"] = version absent = set(result["variables"]) - set(system.variables) if absent: - raise RuntimeError(f"diagnostic names unknown engine variables: {absent}") + if not tolerant: + raise RuntimeError( + f"diagnostic names unknown engine variables: {absent}" + ) + result["absent_variables"] = sorted(absent) + return results + + +def _close(value, target, tolerance=0.5) -> bool: + return isinstance(value, (int, float)) and abs(value - target) <= tolerance + + +def _carers_allowance(o): + low, high = (o.get("annual_carers_allowance_gbp") or [None, None])[:2] + if not all(isinstance(v, (int, float)) for v in (low, high)): + return "inconclusive" + if low > 0 and low == high: + return "persists" # £100,000 of earnings leaves the award unchanged + if low > 0 and high == 0: + return "resolved" + return "inconclusive" + + +def _cgt_commencement(o): + # Announced 10% to 18% basic rate from 30 October 2024: the defect is an + # annual 2024 value carrying none of that part-year increase. + before, after = o.get("2024-10-30", {}), o.get("2025-04-06", {}) + basic, later = before.get("basic"), after.get("basic") + if not all(isinstance(v, (int, float)) for v in (basic, later)): + return "inconclusive" + if _close(basic, 0.10, 1e-9) and _close(later, 0.18, 1e-9): + return "persists" + if 0.10 < basic <= 0.18 and _close(later, 0.18, 1e-9): + return "resolved" + return "inconclusive" + + +def _sdlt_surcharge(o): + rates = o.get("2025-04-06") + if rates == [0.03, 0.05, 0.08, 0.13, 0.15]: + return "persists" + if rates == [0.05, 0.07, 0.10, 0.15, 0.17]: + return "resolved" + return "inconclusive" + + +def _private_school_vat(o): + values = [o.get(d) for d in ("2025-01-01", "2025-06-01", "2026-06-01")] + if any(not isinstance(v, (int, float)) for v in values): + return "inconclusive" + if all(v == 0 for v in values): + return "persists" + if _close(values[1], 0.2, 1e-9) and _close(values[2], 0.2, 1e-9): + return "resolved" + return "inconclusive" + + +def _dividend_lag(o): + pairs = [] + for date in ("2024-06-01", "2025-06-01"): + row = o.get(date) or {} + pairs += [ + (row.get("uk_higher_threshold"), row.get("dividend_higher_threshold")), + ( + row.get("uk_additional_threshold"), + row.get("dividend_additional_threshold"), + ), + ] + if any(not isinstance(v, (int, float)) for pair in pairs for v in pair): + return "inconclusive" + return "persists" if any(a != b for a, b in pairs) else "resolved" + + +def _class4_indexation(o): + # Prior law froze the Class 4 limits until April 2028; the defect is + # uprating that resumes in April 2027. + now, next_year = o.get("2026-06-01") or {}, o.get("2027-06-01") or {} + values = [ + d.get(k) + for d in (now, next_year) + for k in ("lower_profits_limit", "upper_profits_limit") + ] + if any(not isinstance(v, (int, float)) for v in values): + return "inconclusive" + if values[2] > values[0] or values[3] > values[1]: + return "persists" + if values[2] == values[0] and values[3] == values[1]: + return "resolved" + return "inconclusive" + + +def _lcwra_cutoff(o): + element = o.get("annual_synthetic_lcwra_element") or {} + first, second = element.get("2029"), element.get("2030") + if not (first and second) or not all( + isinstance(v, (int, float)) for v in first + second + ): + return "inconclusive" + if second[0] < 0.75 * first[0]: + return "persists" # the protected award falls away in 2030 + if second[0] >= first[0]: + return "resolved" + return "inconclusive" + + +def _employer_nics_spa(o): + employer = o.get("ni_class_1_employer") or [] + if len(employer) != 2 or not all(isinstance(v, (int, float)) for v in employer): + return "inconclusive" + working_age, pension_age = employer + if working_age > 0 and pension_age == 0: + return "persists" + if working_age > 0 and _close(pension_age, working_age, 1.0): + return "resolved" + return "inconclusive" + + +def _pension_taper(o): + allowance = (o.get("pension_annual_allowance") or [None])[0] + if _close(allowance, 60_000): + return "persists" # HMRC's adjusted-income rule gives £45,000 here + if _close(allowance, 45_000): + return "resolved" + return "inconclusive" + + +def _annual_allowance_charge(o): + charge = (o.get("personal_pension_contributions_tax") or [None])[0] + if _close(charge, 4_000): + return "persists" # the band-crossing rule gives £6,460 + if _close(charge, 6_460): + return "resolved" + return "inconclusive" + + +DEFECT_PREDICATES = { + "carers_allowance_earnings_test_absent": _carers_allowance, + "cgt_main_rate_commencement": _cgt_commencement, + "sdlt_additional_home_hike_absent": _sdlt_surcharge, + "private_school_vat_current_law_lever_zero": _private_school_vat, + "dividend_band_threshold_lag": _dividend_lag, + "class4_threshold_indexation_from_2027": _class4_indexation, + "uc_lcwra_protection_stops_at_2030": _lcwra_cutoff, + "employer_nics_state_pension_age_exemption": _employer_nics_spa, + "pension_taper_omits_employer_contributions": _pension_taper, + "annual_allowance_charge_single_marginal_rate": _annual_allowance_charge, +} + + +def defect_status(result: dict) -> str: + """persists | resolved | inconclusive, from the case's own observation. + + A missing engine file or variable is inconclusive: code that moved is not + evidence that the behaviour changed.""" + predicate = DEFECT_PREDICATES.get(result.get("id")) + if predicate is None or result.get("absent_variables"): + return "inconclusive" + evidence = [ + result.get("evidence") or {}, + *(result.get("supporting_evidence") or []), + ] + if any(item.get("missing") for item in evidence): + return "inconclusive" + try: + status = predicate(result.get("observation") or {}) + except (TypeError, ValueError, AttributeError, IndexError): + return "inconclusive" + return status if status in STATUSES else "inconclusive" + + +def reverify(prior: list[dict], *, expect_engine: str) -> list[dict]: + """Rerun every case on the installed engine and record each status.""" + established = {record["id"]: record for record in prior} + results = investigate(None, expect_engine=expect_engine, tolerant=True) + if {r["id"] for r in results} != set(established): + raise RuntimeError( + "re-verification case set differs from the prior diagnostics" + ) + for result in results: + prior_record = established[result["id"]] + result["established_on"] = [prior_record["engine_version"]] + result["reverified_on"] = result["engine_version"] + result["status"] = defect_status(result) + result["status_on_established_observation"] = defect_status(prior_record) return results @@ -447,12 +692,25 @@ def main(argv: list[str] | None = None) -> int: ) parser.add_argument("--output", type=Path) parser.add_argument("--report", type=Path) - args = parser.parse_args(argv) - results = ( - json.loads(args.from_json.read_text()) - if args.from_json - else investigate(args.case) + parser.add_argument( + "--expect-engine", + default=LEGACY_ENGINE, + help="the exact policyengine-uk version that must be installed", + ) + parser.add_argument( + "--reverify", + type=Path, + help="prior MODEL_DIAGNOSTICS.json whose cases to rerun on this engine", ) + args = parser.parse_args(argv) + if args.reverify: + results = reverify( + json.loads(args.reverify.read_text()), expect_engine=args.expect_engine + ) + elif args.from_json: + results = json.loads(args.from_json.read_text()) + else: + results = investigate(args.case, expect_engine=args.expect_engine) if args.case: results = [r for r in results if r["id"] == args.case] if not results: diff --git a/results/uk/events/CONSTRUCTION_PREMISES.json b/results/uk/events/CONSTRUCTION_PREMISES.json new file mode 100644 index 0000000..9739f83 --- /dev/null +++ b/results/uk/events/CONSTRUCTION_PREMISES.json @@ -0,0 +1,2322 @@ +{ + "description": "Executed-world values at each annual lookup (1 January) and premise checks for the frozen event constructions on this engine. A failed check is a construction revision candidate, not a model verdict.", + "engine": { + "policyengine": "5.0.2", + "policyengine_core": "3.27.1", + "policyengine_uk": "2.89.2" + }, + "events": { + "autumn_budget_2024": { + "measures": { + "autumn_budget_2024__capital_gains_main_rates_and_reliefs": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.2, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.2, + "reform_world": 0.2 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.1, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.1, + "reform_world": 0.1 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.2, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.2, + "reform_world": 0.2 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + } + } + }, + "autumn_budget_2024__employer_nics_package": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.138, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.138, + "reform_world": 0.138 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 175, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 175, + "reform_world": 175 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 96, + "reform_world": 96 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 96, + "reform_world": 96 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 96, + "reform_world": 96 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 96, + "reform_world": 96 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 96, + "reform_world": 96 + } + } + } + } + }, + "autumn_budget_2024__private_school_vat_20pct": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "the forward delta changes current law in an in-force year", + "holds": true + }, + "reversal_differs_from_current_law": { + "detail": "not a reversal", + "holds": null + } + }, + "construction": "forward_delta_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + } + } + }, + "autumn_budget_2024__sdlt_additional_dwelling_surcharge_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "the forward delta changes current law in an in-force year", + "holds": true + }, + "reversal_differs_from_current_law": { + "detail": "not a reversal", + "holds": null + } + }, + "construction": "forward_delta_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.03 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.13 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.15 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + } + } + }, + "autumn_budget_2024__winter_fuel_means_test": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + } + } + } + }, + "registry_sha256": "89784d34920fb2d6aae702fb4028f84d7bf87f1694df77a0c9c5d255edbcdd34" + }, + "autumn_statement_2023": { + "measures": { + "autumn_statement_2023__class_1_employee_nics_main_rate_cut_2p": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.12, + "current_law": 0.12, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.12, + "reform_world": 0.12 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + } + } + }, + "autumn_statement_2023__class_2_self_employed_nics_abolition": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.15, + "current_law": 3.15, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 3.15, + "reform_world": 3.15 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 7215.942806707856, + "current_law": 7215.942806707856, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 7215.942806707856, + "reform_world": 7215.942806707856 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7396.336628420124, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 7396.336628420124, + "reform_world": 7396.336628420124 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 6845, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 6845, + "reform_world": 6845 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7105, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 7105, + "reform_world": 7105 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7980.15922330097, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 7980.15922330097, + "reform_world": 7980.15922330097 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 8139.754810238305, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 8139.754810238305, + "reform_world": 8139.754810238305 + } + } + } + } + }, + "autumn_statement_2023__class_4_self_employed_nics_main_rate_cut_1p": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.09, + "current_law": 0.09, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.09, + "reform_world": 0.09 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + } + } + }, + "autumn_statement_2023__lha_reset_to_30th_percentile": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": false, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": false, + "reform_world": false + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": true, + "reform_world": true + } + } + } + } + } + }, + "registry_sha256": "197d5583f0171104c673ca1f40b402a77ca2b003f496de37d6bc913fb9fd1021" + }, + "spring_budget_2023": { + "measures": { + "spring_budget_2023__pension_annual_allowance_package": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + } + } + }, + "spring_budget_2023__uc_childcare_cap_increase": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 646.35, + "current_law": 951, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 951, + "reform_world": 951 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1108.04, + "current_law": 1630, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1630, + "reform_world": 1630 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 689.6, + "current_law": 1014.63, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1014.63, + "reform_world": 1014.63 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1182.39, + "current_law": 1739.37, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1739.37, + "reform_world": 1739.37 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 701.32, + "current_law": 1031.88, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1031.88, + "reform_world": 1031.88 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1202.49, + "current_law": 1768.94, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1768.94, + "reform_world": 1768.94 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 727.97, + "current_law": 1071.09, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1071.09, + "reform_world": 1071.09 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1248.18, + "current_law": 1836.16, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1836.16, + "reform_world": 1836.16 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 727.97, + "current_law": 1071.09, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1071.09, + "reform_world": 1071.09 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1248.18, + "current_law": 1836.16, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 1836.16, + "reform_world": 1836.16 + } + } + } + } + } + }, + "registry_sha256": "0a78bfe32712bd30bdf9137bdf182e82a266c7da0e9885dd61cac6ef99a76357" + }, + "spring_budget_2024": { + "measures": { + "spring_budget_2024__class_1_employee_nics_main_rate_cut_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.12, + "current_law": 0.12, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.12, + "reform_world": 0.12 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + } + } + }, + "spring_budget_2024__class_4_self_employed_nics_main_rate_cut_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.09, + "current_law": 0.09, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.09, + "reform_world": 0.09 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + } + } + }, + "spring_budget_2024__hicbc_threshold_and_taper": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 50000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 50000, + "reform_world": 50000 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + } + } + } + }, + "registry_sha256": "64a3bccfd950e868ffc5d6261bf3903df88a3ebedc9b4444494acc717259817a" + }, + "spring_statement_2025": { + "measures": { + "spring_statement_2025__uc_health_element_freeze_and_new_claimant_cut": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 217.26, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2025": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 217.26, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 437.66, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 447.73, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 456.68, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 465.81, + "current_law": 217.26, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + } + } + }, + "spring_statement_2025__uc_standard_allowance_above_inflation": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "this engine samples every government parameter at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 617.6, + "current_law": 617.6, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 617.6, + "reform_world": 617.6 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 489.23, + "current_law": 489.23, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 489.23, + "reform_world": 489.23 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 393.45, + "current_law": 393.45, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 393.45, + "reform_world": 393.45 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 311.68, + "current_law": 311.68, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 311.68, + "reform_world": 311.68 + } + } + }, + "2025": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 628.1, + "current_law": 628.1, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 628.1, + "reform_world": 628.1 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 497.55, + "current_law": 497.55, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 497.55, + "reform_world": 497.55 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 400.14, + "current_law": 400.14, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 400.14, + "reform_world": 400.14 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 316.98, + "current_law": 316.98, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 316.98, + "reform_world": 316.98 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 651.97, + "current_law": 666.97, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 666.97, + "reform_world": 666.97 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 516.46, + "current_law": 528.34, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 528.34, + "reform_world": 528.34 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 415.35, + "current_law": 424.9, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 424.9, + "reform_world": 424.9 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 329.03, + "current_law": 338.58, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 338.58, + "reform_world": 338.58 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 666.96, + "current_law": 682.3088316082925, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 682.3088316082925, + "reform_world": 682.3088316082925 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 528.33, + "current_law": 540.4906488926418, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 540.4906488926418, + "reform_world": 540.4906488926418 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 424.9, + "current_law": 434.67175817557535, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 434.67175817557535, + "reform_world": 434.67175817557535 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 336.59, + "current_law": 346.36658951067614, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 346.36658951067614, + "reform_world": 346.36658951067614 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 680.3, + "current_law": 695.9539315406156, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 695.9539315406156, + "reform_world": 695.9539315406156 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 538.9, + "current_law": 551.299608963175, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 551.299608963175, + "reform_world": 551.299608963175 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 433.39, + "current_law": 443.3645074165368, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 443.3645074165368, + "reform_world": 443.3645074165368 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 343.32, + "current_law": 353.2933747260321, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 353.2933747260321, + "reform_world": 353.2933747260321 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 693.91, + "current_law": 709.8723475869093, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 709.8723475869093, + "reform_world": 709.8723475869093 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 549.68, + "current_law": 562.3250762763957, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 562.3250762763957, + "reform_world": 562.3250762763957 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 442.06, + "current_law": 452.2313754586829, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 452.2313754586829, + "reform_world": 452.2313754586829 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 350.19, + "current_law": 360.3589058667942, + "fiscal_year_blend": null, + "preserve_calendar_dates": null, + "raw_30_april": 360.3589058667942, + "reform_world": 360.3589058667942 + } + } + } + } + } + }, + "registry_sha256": "d01ce0612f3f24830a8d8ea0abaaa6b2626963316b16750310d303f404fbab5b" + } + }, + "schema_version": 1 +} diff --git a/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json b/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json new file mode 100644 index 0000000..d69cd34 --- /dev/null +++ b/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json @@ -0,0 +1,2322 @@ +{ + "description": "Executed-world values at each annual lookup (1 January) and premise checks for the frozen event constructions on this engine. A failed check is a construction revision candidate, not a model verdict.", + "engine": { + "policyengine": "6.2.6", + "policyengine_core": "3.33.0", + "policyengine_uk": "2.125.1" + }, + "events": { + "autumn_budget_2024": { + "measures": { + "autumn_budget_2024__capital_gains_main_rates_and_reliefs": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "these paths are annualised another way (day-weighted fiscal-year blend or statutory calendar dates): gov.hmrc.cgt.additional_rate, gov.hmrc.cgt.basic_rate, gov.hmrc.cgt.higher_rate", + "holds": false + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.21731506849315071, + "current_law": 0.21731506849315071, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.2, + "reform_world": 0.21731506849315071 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.13463013698630139, + "current_law": 0.13463013698630139, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.1, + "reform_world": 0.13463013698630139 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.21731506849315071, + "current_law": 0.21731506849315071, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.2, + "reform_world": 0.21731506849315071 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.18, + "reform_world": 0.18000000000000002 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.18, + "reform_world": 0.18000000000000002 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24000000000000002 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.18, + "reform_world": 0.18 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24000000000000002 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.18, + "reform_world": 0.18000000000000002 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.cgt.additional_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + }, + "gov.hmrc.cgt.basic_rate": { + "baseline_world": 0.1, + "current_law": 0.18000000000000002, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.18, + "reform_world": 0.18000000000000002 + }, + "gov.hmrc.cgt.higher_rate": { + "baseline_world": 0.2, + "current_law": 0.24, + "fiscal_year_blend": true, + "preserve_calendar_dates": false, + "raw_30_april": 0.24, + "reform_world": 0.24 + } + } + } + } + }, + "autumn_budget_2024__employer_nics_package": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.138, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.138, + "reform_world": 0.138 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 175, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 175, + "reform_world": 175 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96.153846, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 96.153846, + "reform_world": 96.153846 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96.153846, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 96.153846, + "reform_world": 96.153846 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96.153846, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 96.153846, + "reform_world": 96.153846 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96.153846, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 96.153846, + "reform_world": 96.153846 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employer": { + "baseline_world": 0.138, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + }, + "gov.hmrc.national_insurance.class_1.thresholds.secondary_threshold": { + "baseline_world": 175, + "current_law": 96.153846, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 96.153846, + "reform_world": 96.153846 + } + } + } + } + }, + "autumn_budget_2024__private_school_vat_20pct": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "the forward delta changes current law in an in-force year", + "holds": true + }, + "reversal_differs_from_current_law": { + "detail": "not a reversal", + "holds": null + } + }, + "construction": "forward_delta_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.contrib.labour.private_school_vat": { + "baseline_world": 0, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0.2 + } + } + } + } + }, + "autumn_budget_2024__sdlt_additional_dwelling_surcharge_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "the forward delta changes current law in an in-force year", + "holds": true + }, + "reversal_differs_from_current_law": { + "detail": "not a reversal", + "holds": null + } + }, + "construction": "forward_delta_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.03 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.13 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.15 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[0].rate": { + "baseline_world": 0.03, + "current_law": 0.03, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.03, + "reform_world": 0.05 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[1].rate": { + "baseline_world": 0.05, + "current_law": 0.05, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.05, + "reform_world": 0.07 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[2].rate": { + "baseline_world": 0.08, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.1 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[3].rate": { + "baseline_world": 0.13, + "current_law": 0.13, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.13, + "reform_world": 0.15 + }, + "gov.hmrc.stamp_duty.residential.purchase.additional.rate[4].rate": { + "baseline_world": 0.15, + "current_law": 0.15, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.15, + "reform_world": 0.17 + } + } + } + } + }, + "autumn_budget_2024__winter_fuel_means_test": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.winter_fuel_payment.eligibility.require_benefits": { + "baseline_world": false, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + } + } + } + }, + "registry_sha256": "89784d34920fb2d6aae702fb4028f84d7bf87f1694df77a0c9c5d255edbcdd34" + }, + "autumn_statement_2023": { + "measures": { + "autumn_statement_2023__class_1_employee_nics_main_rate_cut_2p": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.12, + "current_law": 0.12, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.12, + "reform_world": 0.12 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + } + } + }, + "autumn_statement_2023__class_2_self_employed_nics_abolition": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.45, + "current_law": 3.45, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 3.45, + "reform_world": 3.45 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 6725, + "current_law": 6725, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 6725, + "reform_world": 6725 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 6725, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 6725, + "reform_world": 6725 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 6845, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 6845, + "reform_world": 6845 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7105, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 7105, + "reform_world": 7105 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7247.088788138113, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 7247.088788138113, + "reform_world": 7247.088788138113 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_2.flat_rate": { + "baseline_world": 3.7, + "current_law": 0, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0, + "reform_world": 0 + }, + "gov.hmrc.national_insurance.class_2.small_profits_threshold": { + "baseline_world": 12570, + "current_law": 7392.0236642935615, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 7392.0236642935615, + "reform_world": 7392.0236642935615 + } + } + } + } + }, + "autumn_statement_2023__class_4_self_employed_nics_main_rate_cut_1p": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.09, + "current_law": 0.09, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.09, + "reform_world": 0.09 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.07, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + } + } + }, + "autumn_statement_2023__lha_reset_to_30th_percentile": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": false, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": false, + "reform_world": false + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.LHA.freeze": { + "baseline_world": true, + "current_law": true, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": true, + "reform_world": true + } + } + } + } + } + }, + "registry_sha256": "197d5583f0171104c673ca1f40b402a77ca2b003f496de37d6bc913fb9fd1021" + }, + "spring_budget_2023": { + "measures": { + "spring_budget_2023__pension_annual_allowance_package": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.allowances.annual_allowance.default": { + "baseline_world": 40000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.minimum": { + "baseline_world": 4000, + "current_law": 10000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 10000, + "reform_world": 10000 + }, + "gov.hmrc.income_tax.allowances.annual_allowance.taper": { + "baseline_world": 240000, + "current_law": 260000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 260000, + "reform_world": 260000 + } + } + } + } + }, + "spring_budget_2023__uc_childcare_cap_increase": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 646.35, + "current_law": 951, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 951, + "reform_world": 951 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1108.04, + "current_law": 1630, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1630, + "reform_world": 1630 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 689.6, + "current_law": 1014.63, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1014.63, + "reform_world": 1014.63 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1182.39, + "current_law": 1739.37, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1739.37, + "reform_world": 1739.37 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 701.32, + "current_law": 1031.88, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1031.88, + "reform_world": 1031.88 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1202.49, + "current_law": 1768.94, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1768.94, + "reform_world": 1768.94 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 727.97, + "current_law": 1071.09, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1071.09, + "reform_world": 1071.09 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1248.18, + "current_law": 1836.16, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1836.16, + "reform_world": 1836.16 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.elements.childcare.cap.1": { + "baseline_world": 727.97, + "current_law": 1071.09, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1071.09, + "reform_world": 1071.09 + }, + "gov.dwp.universal_credit.elements.childcare.cap.2": { + "baseline_world": 1248.18, + "current_law": 1836.16, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 1836.16, + "reform_world": 1836.16 + } + } + } + } + } + }, + "registry_sha256": "0a78bfe32712bd30bdf9137bdf182e82a266c7da0e9885dd61cac6ef99a76357" + }, + "spring_budget_2024": { + "measures": { + "spring_budget_2024__class_1_employee_nics_main_rate_cut_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.12, + "current_law": 0.12, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.12, + "reform_world": 0.12 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_1.rates.employee.main": { + "baseline_world": 0.1, + "current_law": 0.08, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.08, + "reform_world": 0.08 + } + } + } + } + }, + "spring_budget_2024__class_4_self_employed_nics_main_rate_cut_2pp": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.09, + "current_law": 0.09, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.09, + "reform_world": 0.09 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.national_insurance.class_4.rates.main": { + "baseline_world": 0.08, + "current_law": 0.06, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 0.06, + "reform_world": 0.06 + } + } + } + } + }, + "spring_budget_2024__hicbc_threshold_and_taper": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2023": { + "in_force": false, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 50000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 50000, + "reform_world": 50000 + } + } + }, + "2024": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2025": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_end": { + "baseline_world": 60000, + "current_law": 80000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 80000, + "reform_world": 80000 + }, + "gov.hmrc.income_tax.charges.CB_HITC.phase_out_start": { + "baseline_world": 50000, + "current_law": 60000, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 60000, + "reform_world": 60000 + } + } + } + } + } + }, + "registry_sha256": "64a3bccfd950e868ffc5d6261bf3903df88a3ebedc9b4444494acc717259817a" + }, + "spring_statement_2025": { + "measures": { + "spring_statement_2025__uc_health_element_freeze_and_new_claimant_cut": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 217.26, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2025": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 217.26, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 437.66, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 447.73, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 456.68, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.rebalancing.new_claimant_health_element": { + "baseline_world": 465.81, + "current_law": 217.26, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 217.26, + "reform_world": 217.26 + } + } + } + } + }, + "spring_statement_2025__uc_standard_allowance_above_inflation": { + "checks": { + "annual_value_matches_30_april_sample": { + "detail": "every touched path is sampled at 30 April", + "holds": true + }, + "authored_construction_reproduces": { + "detail": "re-authoring with this engine's parameters reproduces the registry", + "holds": true + }, + "forward_delta_changes_current_law": { + "detail": "not a forward delta", + "holds": null + }, + "reversal_differs_from_current_law": { + "detail": "the reversal differs from current law in an in-force year", + "holds": true + } + }, + "construction": "reversal_on_certified_world", + "years": { + "2024": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 617.6, + "current_law": 617.6, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 617.6, + "reform_world": 617.6 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 489.23, + "current_law": 489.23, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 489.23, + "reform_world": 489.23 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 393.45, + "current_law": 393.45, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 393.45, + "reform_world": 393.45 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 311.68, + "current_law": 311.68, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 311.68, + "reform_world": 311.68 + } + } + }, + "2025": { + "in_force": false, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 628.1, + "current_law": 628.1, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 628.1, + "reform_world": 628.1 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 497.55, + "current_law": 497.55, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 497.55, + "reform_world": 497.55 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 400.14, + "current_law": 400.14, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 400.14, + "reform_world": 400.14 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 316.98, + "current_law": 316.98, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 316.98, + "reform_world": 316.98 + } + } + }, + "2026": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 651.97, + "current_law": 666.97, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 666.97, + "reform_world": 666.97 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 516.46, + "current_law": 528.34, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 528.34, + "reform_world": 528.34 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 415.35, + "current_law": 424.9, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 424.9, + "reform_world": 424.9 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 329.03, + "current_law": 338.58, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 338.58, + "reform_world": 338.58 + } + } + }, + "2027": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 666.96, + "current_law": 682.3088316082925, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 682.3088316082925, + "reform_world": 682.3088316082925 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 528.33, + "current_law": 540.4906488926418, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 540.4906488926418, + "reform_world": 540.4906488926418 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 424.9, + "current_law": 434.67175817557535, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 434.67175817557535, + "reform_world": 434.67175817557535 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 336.59, + "current_law": 346.36658951067614, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 346.36658951067614, + "reform_world": 346.36658951067614 + } + } + }, + "2028": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 680.3, + "current_law": 695.9539315406156, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 695.9539315406156, + "reform_world": 695.9539315406156 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 538.9, + "current_law": 551.299608963175, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 551.299608963175, + "reform_world": 551.299608963175 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 433.39, + "current_law": 443.3645074165368, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 443.3645074165368, + "reform_world": 443.3645074165368 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 343.32, + "current_law": 353.2933747260321, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 353.2933747260321, + "reform_world": 353.2933747260321 + } + } + }, + "2029": { + "in_force": true, + "paths": { + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD": { + "baseline_world": 693.91, + "current_law": 709.8723475869093, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 709.8723475869093, + "reform_world": 709.8723475869093 + }, + "gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG": { + "baseline_world": 549.68, + "current_law": 562.3250762763957, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 562.3250762763957, + "reform_world": 562.3250762763957 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD": { + "baseline_world": 442.06, + "current_law": 452.2313754586829, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 452.2313754586829, + "reform_world": 452.2313754586829 + }, + "gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG": { + "baseline_world": 350.19, + "current_law": 360.3589058667942, + "fiscal_year_blend": false, + "preserve_calendar_dates": false, + "raw_30_april": 360.3589058667942, + "reform_world": 360.3589058667942 + } + } + } + } + } + }, + "registry_sha256": "d01ce0612f3f24830a8d8ea0abaaa6b2626963316b16750310d303f404fbab5b" + } + }, + "schema_version": 1 +} diff --git a/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/MODEL_DIAGNOSTICS.json b/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/MODEL_DIAGNOSTICS.json new file mode 100644 index 0000000..8005d6f --- /dev/null +++ b/results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/MODEL_DIAGNOSTICS.json @@ -0,0 +1,593 @@ +[ + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "variables/gov/dwp/carers_allowance.py", + "sha256": "9a37b0a14c412161b9f3f472024199cef2a66fea62403ca5b2d56a52f4a67800", + "source": "from policyengine_uk.model_api import *\n\n\nclass carers_allowance(Variable):\n value_type = float\n entity = Person\n label = \"Carer's Allowance\"\n documentation = (\n \"Carer's Allowance payable after the overlapping-benefit adjustment: \"\n \"the allowance is reduced by any other personal benefit that overlaps \"\n \"with it, such as State Pension, and only the balance is paid. The \"\n \"comparison is made on annual amounts, which equals the weekly rule \"\n \"when the overlapping benefit is paid at a constant rate all year. \"\n \"Where the overlapping benefit is above the carer rate in some weeks \"\n \"and lower or absent in others, for example when it is paid for part \"\n \"of the year, the annual comparison pays less than the weekly rule \"\n \"would; annual inputs carry no dates to do better.\"\n )\n definition_period = YEAR\n unit = GBP\n reference = (\n \"https://www.legislation.gov.uk/ukpga/1992/4/section/70\",\n \"https://www.legislation.gov.uk/uksi/1979/597/regulation/4\",\n \"https://www.legislation.gov.uk/uksi/1979/597/regulation/12\",\n )\n\n def formula(person, period, parameters):\n pre_overlap = person(\"carers_allowance_pre_overlap\", period)\n overlapping = person(\"carers_allowance_overlapping_benefits\", period)\n return max_(pre_overlap - overlapping, 0)\n" + }, + "id": "carers_allowance_earnings_test_absent", + "interpretation": "The formula uses care hours or reported receipt and no earnings test. The AB2024 earnings-limit increase cannot be represented by this formula. This is an encoding gap, not a sized explanation of the national costing.", + "observation": { + "annual_carers_allowance_gbp": [ + 4331.080078125, + 4331.080078125 + ], + "annual_earnings_gbp": [ + 0, + 100000 + ], + "care_hours_weekly": [ + 35, + 35 + ], + "earnings_limit_parameter_names": [ + "min_hours", + "overlapping_benefits", + "qualifying_disability_benefits", + "rate" + ] + }, + "reproducer": "--case carers_allowance_earnings_test_absent", + "reverified_on": "2.125.1", + "status": "persists", + "status_on_established_observation": "persists", + "variables": [ + "carers_allowance" + ] + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "parameters/gov/hmrc/cgt/basic_rate.yaml", + "sha256": "adea67b26f52f53b42b0fa8e333d1b200ea8431524c6d7a2532054daf8f16579", + "source": "description: Capital gains tax rate on basic rate taxpayers. The rates changed part-way through 2024-25, so the value for that year is the day-weighted average of the rates in force before and after 30 October 2024.\nvalues:\n 2015-01-01:\n value: 0.1\n reference:\n - title: Finance Act 2016 s. 83\n href: https://www.legislation.gov.uk/ukpga/2016/24/section/83\n 2024-10-30:\n value: 0.18\n reference:\n - title: Finance Act 2025 s. 7\n href: https://www.legislation.gov.uk/ukpga/2025/8/section/7\nmetadata:\n fiscal_year_blend: true\n unit: /1\n label: Capital Gains Tax basic rate\n reference:\n - title: Finance Act 2025(7)\n href: https://www.legislation.gov.uk/ukpga/2025/8/section/7\n\n" + }, + "id": "cgt_main_rate_commencement", + "interpretation": "The harvested AB2024 title specifies 30 October 2024, while the raw main-rate schedule starts 6 April 2025. CountryTaxBenefitSystem annualizes government policy from the 30 April snapshot. The first higher processed year, 2025, is inferred from that conversion rule and the recorded April 2025 values. This records a missing part-year 2024 commencement, not an additional delay to 2026. The parameter description also warns that it is under active development. The national timing contribution remains unsized.", + "observation": { + "2024-10-29": { + "additional": 0.21731506849315071, + "basic": 0.13463013698630139, + "higher": 0.21731506849315071 + }, + "2024-10-30": { + "additional": 0.21731506849315071, + "basic": 0.13463013698630139, + "higher": 0.21731506849315071 + }, + "2025-04-06": { + "additional": 0.24, + "basic": 0.18000000000000002, + "higher": 0.24 + } + }, + "reproducer": "--case cgt_main_rate_commencement", + "reverified_on": "2.125.1", + "status": "resolved", + "status_on_established_observation": "persists", + "variables": [ + "capital_gains_tax" + ] + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "parameters/gov/hmrc/stamp_duty/residential/purchase/additional/rate.yaml", + "sha256": "0467640e77930ae05b3f923d8ed85eb37f634200e225a49766ec971785c48fb6", + "source": "description: Stamp Duty Land Tax progressive rates for residential property transfers that are not replacements of an individual's primary residence.\nbrackets:\n - rate:\n 2016-09-15: 0.03\n threshold:\n 2016-09-15: 0\n - rate:\n 2016-09-15: 0.05\n 2020-07-08: null\n 2021-03-31: 0.05\n threshold:\n 2016-09-15: 125_000\n 2020-07-08: null\n 2021-03-31: 125_000\n - rate:\n 2016-09-15: 0.08\n threshold:\n 2016-09-15: 250_000\n 2020-07-08: 500_000\n 2021-03-31: 250_000\n - rate:\n 2016-09-15: 0.13\n threshold:\n 2016-09-15: 925_000\n - rate:\n 2016-09-15: 0.15\n threshold:\n 2016-09-15: 1_500_000\nmetadata:\n threshold_unit: currency-GBP\n rate_unit: /1\n label: Stamp Duty on secondary residences\n reference:\n - title: Finance Act 2003 schedule 4ZA(1)(2)\n href: https://www.legislation.gov.uk/ukpga/2003/14/schedule/4ZA\n - title: Stamp Duty Land Tax (Temporary Relief) Act 2020\n href: https://www.legislation.gov.uk/ukpga/2020/15/section/1/enacted\n" + }, + "id": "sdlt_additional_home_hike_absent", + "interpretation": "The source title raises HRAD from 3% to 5% on 31 October 2024. The pinned additional-home scale still has rates 3/5/8/13/15% at these dates. A forward +2pp construction models this missing announced change; it is not a reversal already present in certified law.", + "observation": { + "2024-10-30": [ + 0.03, + 0.05, + 0.08, + 0.13, + 0.15 + ], + "2024-10-31": [ + 0.03, + 0.05, + 0.08, + 0.13, + 0.15 + ], + "2025-04-06": [ + 0.03, + 0.05, + 0.08, + 0.13, + 0.15 + ], + "2026-06-01": [ + 0.03, + 0.05, + 0.08, + 0.13, + 0.15 + ] + }, + "reproducer": "--case sdlt_additional_home_hike_absent", + "reverified_on": "2.125.1", + "status": "persists", + "status_on_established_observation": "persists", + "variables": [ + "sdlt_on_residential_property_transactions", + "stamp_duty_land_tax" + ] + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "parameters/gov/contrib/labour/private_school_vat.yaml", + "sha256": "d108161e9d328757f935e0149c27fb06ead467d2e16f359a4b79ff65b8c26633", + "source": "description: VAT rate applied to private schools.\nvalues:\n 2000-01-01: 0\nmetadata:\n unit: /1\n label: Private school VAT rate" + }, + "id": "private_school_vat_current_law_lever_zero", + "interpretation": "The harvested AB2024 title applies 20% VAT to private-school education and boarding from January 2025. The pinned private-school VAT lever remains zero. The replay therefore executes a forward 20% construction; its imputed fees, attendance and omitted input-VAT recovery remain separate unsized scope limitations.", + "observation": { + "2025-01-01": 0, + "2025-06-01": 0, + "2026-06-01": 0 + }, + "reproducer": "--case private_school_vat_current_law_lever_zero", + "reverified_on": "2.125.1", + "status": "persists", + "status_on_established_observation": "persists", + "variables": [ + "private_school_vat" + ] + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_statement_2023", + "spring_budget_2024", + "spring_budget_2023" + ], + "evidence": { + "engine_file": "parameters/gov/hmrc/income_tax/rates/dividends.yaml", + "sha256": "d3bcbc7fec39442b8313401c9c84f9fe7d704abf070e9a91a4d01478b54f8244", + "source": "description: Dividend income tax scale. Dividends are taxed using this scale as if they were on top of non-dividend income.\nbrackets:\n - rate:\n metadata:\n label: Dividends basic rate\n unit: /1\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n values:\n 2015-04-01: 0.075\n 2022-04-01:\n value: 0.0875\n metadata:\n reference:\n - title: Autumn Budget and Spending Review 2022\n href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf\n 2026-04-06:\n value: 0.1075\n metadata:\n reference:\n - title: OBR Economic and Fiscal Outlook November 2025\n href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/\n threshold:\n values:\n 2015-04-01: 0\n metadata:\n label: Dividends basic rate threshold\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n - rate:\n metadata:\n label: Dividends higher rate\n unit: /1\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n values:\n 2015-04-01: 0.325\n 2022-04-01:\n value: 0.3375\n metadata:\n reference:\n - title: Autumn Budget and Spending Review 2022\n href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf\n 2026-04-06:\n value: 0.3575\n metadata:\n reference:\n - title: OBR Economic and Fiscal Outlook November 2025\n href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/\n threshold:\n metadata:\n label: Dividends higher rate threshold\n values:\n 2015-04-01: 37_500\n 2026-04-06:\n value: 37_700\n metadata:\n reference:\n - title: Income Tax rates and allowances for current and previous tax years\n href: https://www.gov.uk/government/publications/rates-and-allowances-income-tax/income-tax-rates-and-allowances-current-and-past\n - rate:\n metadata:\n label: Dividends additional rate\n unit: /1\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n values:\n 2015-04-01: 0.381\n 2022-04-01:\n value: 0.3935\n metadata:\n reference:\n - title: Autumn Budget and Spending Review 2022\n href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf\n threshold:\n values:\n 2015-04-01: 150_000\n 2026-04-06:\n value: 125_140\n metadata:\n reference:\n - title: Income Tax rates and allowances for current and previous tax years\n href: https://www.gov.uk/government/publications/rates-and-allowances-income-tax/income-tax-rates-and-allowances-current-and-past\n metadata:\n period: year\n label: Dividends additional rate threshold\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n - rate:\n description: An extra dividend tax bracket.\n metadata:\n label: Extra dividend tax bracket rate\n unit: /1\n values:\n 2015-04-01: 0.381\n 2022-06-05: 0.3935\n threshold:\n description: The lower threshold for the extra dividend bracket rate.\n metadata:\n label: Extra dividend tax bracket threshold\n period: year\n unit: currency-GBP\n values:\n 2015-06-05: 10_000_000\nmetadata:\n threshold_period: year\n type: marginal_rate\n threshold_unit: currency-GBP\n rate_unit: /1\n reference:\n - title: Finance Act 2022 s. 4\n href: https://www.legislation.gov.uk/ukpga/2022/3/section/4/enacted\n - title: Autumn Budget and Spending Review\n href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1029973/Budget_AB2021_Print.pdf#page=138\n" + }, + "existing_issue": "https://github.com/PolicyEngine/policyengine-uk/issues/1822", + "id": "dividend_band_threshold_lag", + "interpretation": "The existing mode-2 registry records the dividend thresholds lagging the main bands until April 2026. Their effect on a specific NICs costing's Income Tax interaction is unsized, so the national gap remains open.", + "observation": { + "2024-06-01": { + "dividend_additional_threshold": 150000, + "dividend_higher_threshold": 37500, + "uk_additional_threshold": 125140, + "uk_higher_threshold": 37700 + }, + "2025-06-01": { + "dividend_additional_threshold": 150000, + "dividend_higher_threshold": 37500, + "uk_additional_threshold": 125140, + "uk_higher_threshold": 37700 + }, + "2026-06-01": { + "dividend_additional_threshold": 125140, + "dividend_higher_threshold": 37700, + "uk_additional_threshold": 125140, + "uk_higher_threshold": 37700 + } + }, + "reproducer": "--case dividend_band_threshold_lag", + "reverified_on": "2.125.1", + "status": "persists", + "status_on_established_observation": "persists", + "variables": [ + "dividend_income_tax", + "income_tax" + ] + }, + { + "assessment": "data/uk/ab2025_measures.json: personal_tax_thresholds_freeze_to_2031.baseline_integrity_note", + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_statement_2023", + "spring_budget_2024" + ], + "evidence": { + "engine_file": "parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml", + "sha256": "a97c7fafd284942b70979f6285dcfe7d3e2d92badb770929fa7144787bac7e6d", + "source": "description: The Lower Profits Limit is the threshold at which self-employed earners pay the main Class 4 National Insurance rate.\n# Legal status: \u00a312,570 is the standing figure in SSCBA 1992 s.15(3)(a), which\n# stays until altered by order under SSAA 1992 s.141. Budget 2025 commits to\n# keeping it until April 2031. CPI uprating from 2031-32 is a modelling\n# assumption.\nvalues:\n 2015-04-06:\n value: 8_060\n metadata:\n reference:\n - title: The Social Security (Contributions) (Re-rating and National Insurance Funds Payments) Order 2015 art 3(a)\n href: https://www.legislation.gov.uk/uksi/2015/588/article/3/made\n - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2016, unchanged for 2016-17)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2016-04-06\n 2017-04-06:\n value: 8_164\n metadata:\n reference:\n - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2017 reg 5(a)\n href: https://www.legislation.gov.uk/uksi/2017/415/regulation/5/made\n 2018-04-06:\n value: 8_424\n metadata:\n reference:\n - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2018 reg 5(a)\n href: https://www.legislation.gov.uk/uksi/2018/337/regulation/5/made\n 2019-04-06:\n value: 8_632\n metadata:\n reference:\n - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2019 reg 5(a)\n href: https://www.legislation.gov.uk/uksi/2019/262/regulation/5/made\n 2020-04-06:\n value: 9_500\n metadata:\n reference:\n - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2020 reg 5\n href: https://www.legislation.gov.uk/uksi/2020/299/regulation/5/made\n 2021-04-06:\n value: 9_568\n metadata:\n reference:\n - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2021 reg 5(1)\n href: https://www.legislation.gov.uk/uksi/2021/157/regulation/5/made\n # SI 2022/232 reg 5 first set \u00a39,880 for 2022-23; the 2022 Act replaced it\n # with \u00a311,908 for the whole year.\n 2022-04-06:\n value: 11_908\n metadata:\n reference:\n - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(1) and (2)\n href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted\n 2023-04-06:\n value: 12_570\n metadata:\n reference:\n - title: National Insurance Contributions (Increase of Thresholds) Act 2022 s 2(3) and (4)\n href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted\n - title: Social Security Contributions and Benefits Act 1992 s 15(3)(a) (as at 6 April 2023)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06\n 2026-04-06:\n value: 12_570\n metadata:\n reference:\n - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15\n 2027-04-06:\n value: 12_570\n metadata:\n reference:\n - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15\n - title: \"HMRC: Income Tax Personal Allowance and the basic rate limit, and certain National Insurance contributions thresholds from 6 April 2026 to 5 April 2028 (Autumn Statement 2022)\"\n href: https://www.gov.uk/government/publications/the-personal-allowance-and-basic-rate-limit-for-income-tax-and-certain-national-insurance-contributions-nics-thresholds-from-6-april-2026-to-5-apr/income-tax-personal-allowance-and-the-basic-rate-limit-and-certain-national-insurance-contributions-thresholds-from-6-april-2026-to-5-april-2028\n 2028-04-06:\n value: 12_570\n metadata:\n reference:\n - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15\n - title: Budget 2025 (HC 1492) para 4.111\n href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113\n - title: \"HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031\"\n href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt\n - title: Explanatory Memorandum to SI 2026/231 para 5.25\n href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf\n 2029-04-06:\n value: 12_570\n metadata:\n reference:\n - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15\n - title: Budget 2025 (HC 1492) para 4.111\n href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113\n - title: \"HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031\"\n href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt\n - title: Explanatory Memorandum to SI 2026/231 para 5.25\n href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf\n 2030-04-06:\n value: 12_570\n metadata:\n reference:\n - title: Social Security Contributions and Benefits Act 1992 s.15(3)(a)\n href: https://www.legislation.gov.uk/ukpga/1992/4/section/15\n - title: Budget 2025 (HC 1492) para 4.111\n href: https://assets.publishing.service.gov.uk/media/6929b353345e31ab14ecf735/E03444720_Budget_2025_Web_Accessible.pdf#page=113\n - title: \"HMRC: Maintaining Income Tax and equivalent National Insurance contributions thresholds until 5 April 2031\"\n href: https://www.gov.uk/government/publications/maintaining-income-tax-and-equivalent-national-insurance-contributions-thresholds-until-5-april-2031/income-tax-maintaining-the-personal-allowance-and-the-basic-rate-limit-for-income-tax-and-equivalent-national-insurance-contributions-thresholds-unt\n - title: Explanatory Memorandum to SI 2026/231 para 5.25\n href: https://www.legislation.gov.uk/uksi/2026/231/pdfs/uksiem_20260231_en_001.pdf\nmetadata:\n label: NI lower profits limit\n unit: currency-GBP\n period: year\n uprating: gov.economic_assumptions.indices.obr.consumer_price_index\n reference:\n - https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/882271/Table-a4.pdf\n - https://www.gov.uk/government/publications/rates-and-allowances-national-insurance-contributions/rates-and-allowances-national-insurance-contributions\n" + }, + "id": "class4_threshold_indexation_from_2027", + "interpretation": "The existing AB2025 baseline integrity assessment states the prior-law Class 4 freeze runs to April 2028 while the pin resumes uprating in April 2027. This changes the marginal tax base in 2027; no fraction of a replay gap is attributed without a paired run.", + "national_effect": "unsized", + "observation": { + "2026-06-01": { + "lower_profits_limit": 12570, + "upper_profits_limit": 50270 + }, + "2027-06-01": { + "lower_profits_limit": 12570, + "upper_profits_limit": 50270 + }, + "2028-06-01": { + "lower_profits_limit": 12570, + "upper_profits_limit": 50270 + } + }, + "primary_source": { + "checked_date": "2026-10-09", + "rule": "The Class 4 Lower Profits Limit of \u00a312,570 and Upper Profits Limit of \u00a350,270 were frozen until April 2028.", + "title": "Autumn Statement 2022 tax section", + "url": "https://www.gov.uk/government/publications/autumn-statement-2022-documents/autumn-statement-2022-html" + }, + "reproducer": "--case class4_threshold_indexation_from_2027", + "reverified_on": "2.125.1", + "status": "resolved", + "status_on_established_observation": "persists", + "variables": [ + "ni_class_4" + ] + }, + { + "class": "investigation", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "spring_statement_2025" + ], + "evidence": { + "engine_file": "scenarios/uc_reform.py", + "sha256": "f0b259519aed379720c14cc039cec1f4efca9b8d61b301ef5f67f667bbd1da9c", + "source": "from policyengine_uk.model_api import Scenario\nfrom policyengine_uk import Microsimulation\nfrom policyengine_uk.variables.gov.dwp.universal_credit.standard_allowance.uc_standard_allowance_claimant_type import (\n UCClaimantType,\n)\nimport numpy as np\n\n\nBASELINE_UC_REBALANCING_YEAR = 2025\n\n\ndef _benefit_uprating_ratio(sim: Microsimulation, year: int) -> float:\n parameters = sim.tax_benefit_system.parameters\n current_index = float(parameters(str(year)).gov.benefit_uprating_cpi)\n baseline_index = float(\n parameters(str(BASELINE_UC_REBALANCING_YEAR)).gov.benefit_uprating_cpi\n )\n return current_index / baseline_index\n\n\ndef _rebalanced_standard_allowance_monthly(\n sim: Microsimulation, year: int, claimant_type: str\n) -> float:\n current = sim.tax_benefit_system.parameters(str(year))\n return float(\n current.gov.dwp.universal_credit.standard_allowance.amount[claimant_type]\n )\n\n\ndef _protected_existing_health_element_monthly(\n sim: Microsimulation, year: int\n) -> float:\n baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR))\n protected_combined_award = _benefit_uprating_ratio(sim, year) * (\n float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD)\n + float(baseline.gov.dwp.universal_credit.elements.disabled.amount)\n )\n return protected_combined_award - _rebalanced_standard_allowance_monthly(\n sim, year, \"SINGLE_OLD\"\n )\n\n\ndef _protected_single_young_health_element_monthly(\n sim: Microsimulation, year: int\n) -> float:\n baseline = sim.tax_benefit_system.parameters(str(BASELINE_UC_REBALANCING_YEAR))\n protected_combined_award = _benefit_uprating_ratio(sim, year) * (\n float(baseline.gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG)\n + float(baseline.gov.dwp.universal_credit.elements.disabled.amount)\n )\n return protected_combined_award - _rebalanced_standard_allowance_monthly(\n sim, year, \"SINGLE_YOUNG\"\n )\n\n\ndef add_universal_credit_reform(sim: Microsimulation):\n rebalancing = sim.tax_benefit_system.parameters.gov.dwp.universal_credit.rebalancing\n\n generator = np.random.default_rng(43)\n\n uc_seed = generator.random(len(sim.calculate(\"benunit_id\")))\n post_2025_claimant_share = {\n 2025: 0,\n 2026: 0.11,\n 2027: 0.13,\n 2028: 0.16,\n 2029: 0.22,\n } # WPI Economics for Trussell Trust based on admin PIP data, 2025\n new_claimant_health_element = rebalancing.new_claimant_health_element\n for year in range(2026, 2030):\n if not rebalancing.active(year):\n continue\n is_post_2025_claimant = uc_seed < post_2025_claimant_share[year]\n # Copy: calculate returns the simulation's cached array, and the writes\n # below must reach the cache only through set_input.\n current_health_element = np.array(sim.calculate(\"uc_LCWRA_element\", year))\n claimant_type = sim.calculate(\"uc_standard_allowance_claimant_type\", year)\n has_health_element = current_health_element > 0\n protected_health_element = np.full(\n current_health_element.shape,\n _protected_existing_health_element_monthly(sim, year) * 12,\n dtype=current_health_element.dtype,\n )\n protected_health_element[claimant_type == UCClaimantType.SINGLE_YOUNG.name] = (\n _protected_single_young_health_element_monthly(sim, year) * 12\n )\n current_health_element[has_health_element & ~is_post_2025_claimant] = (\n protected_health_element[has_health_element & ~is_post_2025_claimant]\n )\n # Set post-April 2026 claimants to \u00a3217.26/month.\n # https://bills.parliament.uk/publications/62123/documents/6889#page=16\n current_health_element[has_health_element & is_post_2025_claimant] = (\n new_claimant_health_element(year) * 12\n ) # Monthly amount * 12\n sim.set_input(\"uc_LCWRA_element\", year, current_health_element)\n\n # Standard allowance uplift is handled in the formula itself so user\n # reforms to the base amount are applied before the uplift.\n\n\nuniversal_credit_july_2025_reform = Scenario(\n simulation_modifier=add_universal_credit_reform,\n)\n" + }, + "id": "uc_lcwra_protection_stops_at_2030", + "interpretation": "The default Simulation constructor executes a deterministic UC modifier. It protects seeded existing claimants for 2026\u20132029, so inspecting uc_LCWRA_element alone would incorrectly claim all claimants are halved. The modifier loops over range(2026,2030), leaving CY2030 outside that protection override. The synthetic observation isolates the cutoff; continued-protection policy scope requires review before calling it an encoding defect.", + "observation": { + "annual_synthetic_lcwra_element": { + "2029": [ + 5447.29150390625 + ], + "2030": [ + 2830.311279296875 + ] + }, + "formula_source": " def formula(benunit, period, parameters):\n p = parameters(period).gov.dwp.universal_credit.elements.disabled\n person = benunit.members\n limited_capability = person(\"uc_limited_capability_for_WRA\", period)\n # The element is for \"a claimant\" with limited capability for work and\n # work-related activity (reg. 27(1)). A partner who cannot be a joint\n # claimant, so that the other member claims as a single person\n # (reg. 3(3)), is not a claimant.\n not_a_claimant = other_member_of_single_claim(person, period)\n person_amounts = (limited_capability & ~not_a_claimant) * p.amount\n return benunit.sum(person_amounts) * MONTHS_IN_YEAR\n", + "monthly_disabled_element": { + "2025-06-01": 423.27, + "2026-06-01": 217.26, + "2030-06-01": 235.85927653841142 + } + }, + "reproducer": "--case uc_lcwra_protection_stops_at_2030", + "reverified_on": "2.125.1", + "status": "persists", + "status_on_established_observation": "persists", + "variables": [ + "uc_LCWRA_element" + ] + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "variables/gov/hmrc/national_insurance/class_1/ni_class_1_employer.py", + "sha256": "ce285edfa2bdc447fa2033dffef9f066d9f92ec1224045fdea9f87629e5336c1", + "source": "from policyengine_uk.model_api import *\n\n\nclass ni_class_1_employer(Variable):\n value_type = float\n entity = Person\n label = \"NI Class 1 employer-side contributions\"\n definition_period = YEAR\n unit = GBP\n defined_for = \"ni_class_1_secondary_liable\"\n reference = \"https://www.legislation.gov.uk/ukpga/1992/4/section/9\"\n\n def formula(person, period, parameters):\n class_1 = parameters(period).gov.hmrc.national_insurance.class_1\n earnings = person(\"ni_class_1_income\", period)\n if not parameters(\n period\n ).gov.contrib.policyengine.employer_ni.exempt_employer_pension_contributions:\n added_pension_contributions = person(\n \"employer_pension_contributions\", period\n )\n taxed_earnings = earnings + added_pension_contributions\n else:\n taxed_earnings = earnings\n secondary_threshold = class_1.thresholds.secondary_threshold * WEEKS_IN_YEAR\n main_earnings = max_(\n taxed_earnings - secondary_threshold,\n 0,\n )\n return class_1.rates.employer * main_earnings\n" + }, + "id": "employer_nics_state_pension_age_exemption", + "interpretation": "The employer formula uses the same ni_liable age mask as employee contributions. For otherwise identical \u00a350,000 earnings, employer NICs falls to zero at age 70. HMRC states employer contributions continue after State Pension age. This is an employer-liability encoding concern; its national contribution to the NICs replay difference remains unsized.", + "national_effect": "unsized", + "observation": { + "ni_class_1_employee": [ + 2994.4033203125, + 0.0 + ], + "ni_class_1_employer": [ + 6750.00048828125, + 6750.00048828125 + ], + "ni_liable": [ + true, + false + ] + }, + "primary_source": { + "checked_date": "2026-10-09", + "rule": "Employers continue paying National Insurance after employees reach State Pension age.", + "url": "https://www.gov.uk/employee-reaches-state-pension-age" + }, + "reproducer": "--case employer_nics_state_pension_age_exemption", + "reverified_on": "2.125.1", + "situation": { + "benunits": { + "pension_age": { + "members": [ + "pension_age" + ] + }, + "working_age": { + "members": [ + "working_age" + ] + } + }, + "households": { + "pension_age": { + "members": [ + "pension_age" + ], + "region": { + "2026": "LONDON" + } + }, + "working_age": { + "members": [ + "working_age" + ], + "region": { + "2026": "LONDON" + } + } + }, + "people": { + "pension_age": { + "age": { + "2026": 70 + }, + "employment_income": { + "2026": 50000 + }, + "ni_class_1_income": { + "2026": 50000 + } + }, + "working_age": { + "age": { + "2026": 45 + }, + "employment_income": { + "2026": 50000 + }, + "ni_class_1_income": { + "2026": 50000 + } + } + } + }, + "status": "resolved", + "status_on_established_observation": "persists", + "supporting_evidence": [ + { + "engine_file": "variables/gov/hmrc/national_insurance/class_1/ni_liable.py", + "sha256": "0e6d8c0a1137c70ee04d740a3ab57aa2b781cbe92afc4f60b1edadb4f65bcd27", + "source": "from policyengine_uk.model_api import *\n\n\nclass ni_liable(Variable):\n label = \"age-liable for primary Class 1 and Class 2 NI\"\n documentation = (\n \"Whether this person's age allows primary (employee) Class 1 and \"\n \"Class 2 contributions: 16 or over and under state pension age. \"\n \"Primary Class 1 ends at pensionable age (SSCBA 1992 s.6(3)) and Class \"\n \"2 after the week the earner reaches it (s.11(7)(b)). The model \"\n \"applies both with the annual is_SP_age flag. Class 4 ends for anyone \"\n \"over pensionable age at the start of the tax year (SI 2001/1004 reg \"\n \"91(a)); see ni_class_4_liable. Secondary (employer) Class 1 has no \"\n \"upper age limit; see ni_class_1_secondary_liable.\"\n )\n entity = Person\n definition_period = YEAR\n value_type = bool\n reference = [\n \"https://www.legislation.gov.uk/ukpga/1992/4/section/6\",\n \"https://www.legislation.gov.uk/ukpga/1992/4/section/11\",\n ]\n\n def formula(person, period, parameters):\n return person(\"over_16\", period) & ~person(\"is_SP_age\", period)\n" + } + ], + "variables": [ + "ni_liable", + "ni_class_1_employer", + "ni_class_1_employee" + ], + "year": 2026 + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "spring_budget_2023", + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "variables/gov/hmrc/income_tax/allowances/pension_annual_allowance.py", + "sha256": "086e8e8b7da2d5c8693e9f6ce5cb67d6535fbac9b9c619adbe3bab872bad78c2", + "source": "from policyengine_uk.model_api import *\n\n\nclass pension_annual_allowance(Variable):\n value_type = float\n entity = Person\n label = \"Annual Allowance for pension contributions\"\n definition_period = YEAR\n unit = GBP\n\n def formula(person, period, parameters):\n allowance = parameters(period).gov.hmrc.income_tax.allowances.annual_allowance\n ANI = person(\"adjusted_net_income\", period)\n reduction = max_(0, ANI - allowance.taper) * allowance.reduction_rate\n return max_(allowance.minimum, allowance.default - reduction)\n" + }, + "id": "pension_taper_omits_employer_contributions", + "interpretation": "The pension taper uses adjusted_net_income, which excludes employer pension contributions, and omits the separate threshold-income gate. With \u00a3240,000 salary and \u00a350,000 employer contributions it returns the full \u00a360,000 allowance. HMRC's adjusted-income rule gives \u00a3290,000 and a \u00a345,000 allowance for this controlled case. The national contribution to pension or Income Tax costing differences remains unsized.", + "national_effect": "unsized", + "observation": { + "adjusted_net_income": [ + 240000.0 + ], + "pension_annual_allowance": [ + 60000.0 + ] + }, + "primary_source": { + "checked_date": "2026-10-09", + "rule": "Threshold income must exceed \u00a3200,000, and adjusted income adds employer pension contributions; the taper starts above \u00a3260,000.", + "url": "https://www.gov.uk/guidance/pension-schemes-work-out-your-tapered-annual-allowance" + }, + "reproducer": "--case pension_taper_omits_employer_contributions", + "reverified_on": "2.125.1", + "situation": { + "benunits": { + "worker": { + "members": [ + "worker" + ] + } + }, + "households": { + "worker": { + "members": [ + "worker" + ], + "region": { + "2024": "LONDON" + } + } + }, + "people": { + "worker": { + "age": { + "2024": 45 + }, + "employer_pension_contributions": { + "2024": 50000 + }, + "employment_income": { + "2024": 240000 + }, + "personal_pension_contributions": { + "2024": 0 + } + } + } + }, + "status": "persists", + "status_on_established_observation": "persists", + "supporting_evidence": [ + { + "engine_file": "variables/gov/hmrc/income_tax/adjusted_net_income.py", + "sha256": "e981d92c0340fc34e3cba57b781d6fa9afe3fd5fa199989955bd85b79661eeba", + "source": "from policyengine_uk.model_api import *\n\n\nclass adjusted_net_income(Variable):\n value_type = float\n entity = Person\n label = \"Taxable income after tax reliefs and before allowances\"\n definition_period = YEAR\n reference = dict(\n title=\"Income Tax Act 2007, s. 23\",\n href=\"https://www.legislation.gov.uk/ukpga/2007/3/section/23\",\n )\n unit = GBP\n\n def formula(person, period, parameters):\n adjusted_net_income_components = parameters(\n period\n ).gov.hmrc.income_tax.adjusted_net_income_components\n\n # Find adjusted net income\n ani = add(person, period, adjusted_net_income_components)\n\n # For basic income contributions, add basic income\n # Modifying param list directly is mutative, hence two-step process\n if parameters(\n period\n ).gov.contrib.ubi_center.basic_income.interactions.include_in_taxable_income:\n ani += person(\"basic_income\", period)\n\n return max_(0, ani)\n" + }, + { + "engine_file": "parameters/gov/hmrc/income_tax/adjusted_net_income_components.yaml", + "sha256": "27ada6022094b53bc6d47396cabe05d84e7ae2f921d99efa08fcd052d665674b", + "source": "description: Components of adjusted net income.\nvalues:\n 0000-01-01:\n - taxable_employment_income\n - taxable_pension_income\n - taxable_social_security_income\n - taxable_self_employment_income\n - taxable_property_income\n - taxable_savings_interest_income\n - taxable_dividend_income\n - taxable_miscellaneous_income\n \nmetadata:\n unit: list\n label: Adjusted net income components\n period: year\n reference:\n - title: \"Income Tax Act 2007, s. 58 (1)\"\n href: https://www.legislation.gov.uk/ukpga/2007/3/section/58\n" + }, + { + "engine_file": "parameters/gov/hmrc/income_tax/allowances/annual_allowance/taper.yaml", + "sha256": "e163e82307af00d74759b595f594030e2069e383424729f75e9385ad66297a0e", + "source": "description: Adjusted net income limit for tapering of the annual allowance.\nvalues:\n 2015-04-01: 240_000\n 2023-04-06: 260_000\nmetadata:\n unit: currency-GBP\n label: Annual allowance taper threshold\n reference:\n - title: Finance Act 2004 s. 228ZA\n href: https://www.legislation.gov.uk/ukpga/2004/12/section/228ZA\n - title: Tax on your private pension contributions\n href: https://www.gov.uk/tax-on-your-private-pension/annual-allowance\n" + }, + { + "engine_file": "parameters/gov/hmrc/income_tax/allowances/annual_allowance/default.yaml", + "sha256": "4c0b1bd675d5b645103fe6677ee3cb9ed932fcde6970eb3de212f50c5684ba5c", + "source": "description: Annual allowance for relief on pension contributions.\nvalues:\n 2015-04-01: 40_000\n 2023-04-06: 60_000\nmetadata:\n unit: currency-GBP\n label: Annual allowance for pension contributions\n reference:\n - title: Finance Act 2004 s. 228\n href: https://www.legislation.gov.uk/ukpga/2004/12/section/228\n - title: Tax on your private pension contributions\n href: https://www.gov.uk/tax-on-your-private-pension/annual-allowance\n" + } + ], + "variables": [ + "adjusted_net_income", + "pension_annual_allowance" + ], + "year": 2024 + }, + { + "class": "pe_gap", + "engine_version": "2.125.1", + "established_on": [ + "2.89.2" + ], + "events": [ + "spring_budget_2023", + "autumn_budget_2024" + ], + "evidence": { + "engine_file": "variables/gov/hmrc/pensions/private_pension_contributions_tax.py", + "sha256": "eb27b9fc08eb6a6db2d12ea6b7b3e41e620687caa723bb5f9cb0f93661cfdae3", + "source": "from policyengine_uk.model_api import *\n\n\nclass personal_pension_contributions_tax(Variable):\n value_type = float\n entity = Person\n label = \"Reduction in taxable income from pension contributions to pensions other than the State Pension\"\n definition_period = YEAR\n reference = [\n dict(\n title=\"Finance Act 2004 s. 227\",\n href=\"https://www.legislation.gov.uk/ukpga/2004/12/section/227\",\n ),\n dict(\n title=\"Finance Act 2004 s. 233\",\n href=\"https://www.legislation.gov.uk/ukpga/2004/12/section/233\",\n ),\n ]\n unit = GBP\n\n def formula(person, period, parameters):\n p = parameters(period).gov.hmrc.income_tax.rates.uk\n taxed_income = person(\"taxed_income\", period)\n\n personal_pension_contributions = person(\n \"pension_contributions_for_annual_allowance\", period\n )\n pension_annual_allowance = person(\"pension_annual_allowance\", period)\n taxable_contributions = (\n personal_pension_contributions - pension_annual_allowance\n )\n\n tax_rate = p.marginal_rates(taxed_income)\n return max_(0, taxable_contributions * tax_rate)\n" + }, + "id": "annual_allowance_charge_single_marginal_rate", + "interpretation": "The annual-allowance charge applies one marginal rate at taxed_income to the entire excess. Controlled intermediate inputs of \u00a330,000 taxed income and \u00a320,000 excess produce \u00a34,000. HMRC's band-crossing rule gives \u00a36,460 (\u00a37,700 at 20% and \u00a312,300 at 40%). Intermediate overrides isolate this formula rather than claim a complete household contribution history. The national contribution remains unsized.", + "national_effect": "unsized", + "observation": { + "personal_pension_contributions_tax": [ + 4000.0 + ] + }, + "primary_source": { + "checked_date": "2026-10-09", + "rule": "The annual-allowance charge uses the rates that apply if the excess is added to taxable income, including bands crossed by that excess.", + "url": "https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm056110" + }, + "reproducer": "--case annual_allowance_charge_single_marginal_rate", + "reverified_on": "2.125.1", + "situation": { + "benunits": { + "worker": { + "members": [ + "worker" + ] + } + }, + "households": { + "worker": { + "members": [ + "worker" + ], + "region": { + "2024": "LONDON" + } + } + }, + "people": { + "worker": { + "age": { + "2024": 45 + }, + "pension_annual_allowance": { + "2024": 60000 + }, + "pension_contributions_for_annual_allowance": { + "2024": 80000 + }, + "taxed_income": { + "2024": 30000 + } + } + } + }, + "status": "persists", + "status_on_established_observation": "persists", + "supporting_evidence": [ + { + "engine_file": "parameters/gov/hmrc/income_tax/rates/uk.yaml", + "sha256": "c32409413c8739fbee4c12a7038cc523a84728ebee70b35551159705d1c51130", + "source": "brackets:\n- rate:\n description: The basic rate is the first of three tax brackets on all income,\n after allowances are deducted\n metadata:\n label: Basic rate\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2022/3/section/2/enacted\n title: Finance Act 2022 s. 2 (a)\n unit: /1\n values:\n 2015-04-01: 0.2\n threshold:\n description: The starting threshold for the basic rate, after allowances.\n metadata:\n reference:\n - title: Income Tax Act 2007, s. 6 (1)(b)\n href: https://www.legislation.gov.uk/ukpga/2007/3/section/6\n values:\n 2015-04-05: 0\n- rate:\n description: The higher rate is the middle tax bracket on earned income.\n metadata:\n label: Higher rate\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2022/3/section/2/enacted\n title: Finance Act 2022 s. 2 (b)\n unit: /1\n values:\n 2015-04-05: 0.4\n threshold:\n description: The lower threshold for the higher rate of income tax (and therefore\n the upper threshold of the basic rate)\n metadata:\n label: Higher rate threshold\n period: year\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2021/26/section/5\n title: Finance Act 2021 s. 5 (1)\n unit: currency-GBP\n uprating: gov.benefit_uprating_cpi\n values:\n 2015-04-05: 32000\n 2017-04-01: 33500\n 2018-04-01: 34500\n 2019-04-01: 37500\n 2020-04-01: 37500\n 2021-04-01: 37700\n 2027-04-01: 37700\n 2030-04-01:\n value: 37700\n metadata:\n reference:\n - title: OBR Economic and Fiscal Outlook November 2025\n href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/\n- rate:\n description: The additional rate is the highest tax bracket, with no upper bound\n metadata:\n label: Additional rate\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2022/3/section/2/enacted\n title: Finance Act 2022 s. 2 (c)\n tests:\n - decreases_net_income: true\n period: 2022\n unit: /1\n values:\n 2015-04-05: 0.45\n threshold:\n description: The lower threshold for the additional rate\n metadata:\n label: Additional rate threshold\n period: year\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2022/3/section/2/enacted\n title: Finance Act 2022 s. 2\n unit: currency-GBP\n values:\n 2015-04-05: 150_000\n 2023-01-01:\n value: 125_140\n reference:\n - title: Finance Act 2023 s. 6 (4)\n href: https://www.legislation.gov.uk/ukpga/2023/1/section/6/enacted\n 2030-04-01:\n value: 125_140\n metadata:\n reference:\n - title: OBR Economic and Fiscal Outlook November 2025\n href: https://obr.uk/efo/economic-and-fiscal-outlook-november-2025/\n- rate:\n description: An extra tax bracket.\n metadata:\n label: Extra tax bracket rate\n unit: /1\n values:\n 2015-04-05: 0.45\n threshold:\n description: The lower threshold for the extra bracket rate.\n metadata:\n label: Extra tax bracket threshold\n period: year\n unit: currency-GBP\n values:\n 2015-04-05: 10_000_000\ndescription: Income Tax scale.\nmetadata:\n label: Income Tax main rates\n rate_unit: /1\n reference:\n - href: https://www.legislation.gov.uk/ukpga/2022/3/section/2/enacted\n title: Finance Act 2022 s. 2\n threshold_period: year\n threshold_unit: currency-GBP\n type: marginal_rate\n" + } + ], + "variables": [ + "personal_pension_contributions_tax" + ], + "year": 2024 + } +] diff --git a/tests/test_uk_event_model_diagnostics.py b/tests/test_uk_event_model_diagnostics.py index 1d4e06e..95b4b45 100644 --- a/tests/test_uk_event_model_diagnostics.py +++ b/tests/test_uk_event_model_diagnostics.py @@ -35,7 +35,10 @@ def __del__(self): monkeypatch.setattr( diagnostics, "_source", - lambda root, path: {"engine_file": path, "sha256": "recorded separately"}, + lambda root, path, **options: { + "engine_file": path, + "sha256": "recorded separately", + }, ) rows = diagnostics._additional_model_checks(Path("unused"), SmallSimulation, None) assert len(rows) == len(constructed) == 3 diff --git a/tests/test_uk_event_premises.py b/tests/test_uk_event_premises.py new file mode 100644 index 0000000..815602a --- /dev/null +++ b/tests/test_uk_event_premises.py @@ -0,0 +1,252 @@ +"""Construction premises: what each frozen world holds on an engine. + +A stub engine view stands in for policyengine-uk, so the checks run without +the engine; one engine-marked test regenerates the committed 2.125.1 file. +""" + +import importlib.util +import json +import random +from pathlib import Path + +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from pipeline import audit_uk_event_premises as premises +from pipeline import compute_uk_ab2025 as worlds + +ROOT = Path(__file__).resolve().parent.parent + + +class StubEngine: + def __init__(self, law, *, blend=(), reads_blend=True, authored=None): + self.law = law # path -> value (constant) or {year: value} + self.blend = set(blend) + self.reads_blend = reads_blend + self.reads_calendar = reads_blend + self.authored = authored + + def current_law(self, path, date): + value = self.law.get(path) + value = value.get(int(date[:4])) if isinstance(value, dict) else value + return premises.scalar(value) + + def raw_30_april(self, path, year): + return self.current_law(path, f"{year}-04-30") + + def annualisation(self, path): + return { + "fiscal_year_blend": (path in self.blend) if self.reads_blend else None, + "preserve_calendar_dates": False if self.reads_calendar else None, + } + + def resolve(self, path, date): + return self.current_law(path, date) + + +def measure(construction, *, modifier=None, delta=None, commences="2024-25"): + m = { + "measure_key": "event__m", + "title": "Untitled measure", + "computability": "partial", + "construction": construction, + "commences_fy": commences, + "heads": [], + } + if modifier is not None: + m["pe_baseline_modifier"] = modifier + if delta is not None: + m["pe_reform_delta"] = delta + return m + + +def run(m, engine, years=(2023, 2024, 2025)): + world = worlds.worlds_for(m, {m["measure_key"]: m}, years=list(years)) + return premises.audit_measure("event", m, world, list(years), engine) + + +def test_forward_delta_equal_to_current_law_is_inert(): + m = measure( + "forward_delta_on_certified_world", delta={"gov.vat": {"2024-01-01": 0.2}} + ) + moving = run(m, StubEngine({"gov.vat": 0})) + assert moving["checks"]["forward_delta_changes_current_law"]["holds"] is True + assert moving["years"]["2024"]["paths"]["gov.vat"] == { + "current_law": 0, + "baseline_world": 0, + "reform_world": 0.2, + "raw_30_april": 0, + "fiscal_year_blend": False, + "preserve_calendar_dates": False, + } + inert = run(m, StubEngine({"gov.vat": 0.2})) + assert inert["checks"]["forward_delta_changes_current_law"]["holds"] is False + assert inert["checks"]["reversal_differs_from_current_law"]["holds"] is None + + +def test_reversal_identical_to_current_law_is_inert(): + m = measure( + "reversal_on_certified_world", modifier={"gov.rate": {"2024-01-01": 0.1}} + ) + assert ( + run(m, StubEngine({"gov.rate": 0.18}))["checks"][ + "reversal_differs_from_current_law" + ]["holds"] + is True + ) + assert ( + run(m, StubEngine({"gov.rate": 0.1}))["checks"][ + "reversal_differs_from_current_law" + ]["holds"] + is False + ) + + +def test_only_in_force_years_count_for_inertness(): + # Current law equals the reversal only before commencement: not inert. + m = measure( + "reversal_on_certified_world", + modifier={"gov.rate": {"2023-01-01": 0.1}}, + commences="2025-26", + ) + law = {"gov.rate": {2023: 0.1, 2024: 0.1, 2025: 0.18}} + result = run(m, StubEngine(law)) + assert [result["years"][y]["in_force"] for y in ("2023", "2024", "2025")] == [ + False, + False, + True, + ] + assert result["checks"]["reversal_differs_from_current_law"]["holds"] is True + law = {"gov.rate": {2023: 0.18, 2024: 0.18, 2025: 0.1}} + assert ( + run(m, StubEngine(law))["checks"]["reversal_differs_from_current_law"]["holds"] + is False + ) + + +def test_blended_annualisation_is_reported_only_where_the_engine_blends(): + m = measure( + "reversal_on_certified_world", modifier={"gov.cgt": {"2024-10-30": 0.1}} + ) + blended = run(m, StubEngine({"gov.cgt": 0.1346}, blend={"gov.cgt"})) + check = blended["checks"]["annual_value_matches_30_april_sample"] + assert check["holds"] is False and "gov.cgt" in check["detail"] + sampled = run(m, StubEngine({"gov.cgt": 0.1}, reads_blend=False)) + assert sampled["checks"]["annual_value_matches_30_april_sample"]["holds"] is True + assert sampled["years"]["2025"]["paths"]["gov.cgt"]["fiscal_year_blend"] is None + + +def test_october_dates_execute_from_the_next_january(): + m = measure( + "reversal_on_certified_world", modifier={"gov.cgt": {"2024-10-30": 0.1}} + ) + result = run(m, StubEngine({"gov.cgt": 0.18})) + assert result["years"]["2024"]["paths"]["gov.cgt"]["baseline_world"] == 0.18 + assert result["years"]["2025"]["paths"]["gov.cgt"]["baseline_world"] == 0.1 + + +def test_authored_drift_is_caught(monkeypatch): + m = measure( + "reversal_on_certified_world", + modifier={"gov.cap": {"2024": 646.35, "2025": round(646.35 * 1030 / 951, 2)}}, + ) + + def authored(event, title, resolve): + return { + "pe_baseline_modifier": { + "gov.cap": { + str(y): round(646.35 * resolve("gov.cap", f"{y}-04-06") / 951, 2) + for y in (2024, 2025) + } + } + } + + monkeypatch.setattr(premises.builder, "authored_construction", authored) + same = run(m, StubEngine({"gov.cap": {2024: 951, 2025: 1030.0}})) + assert same["checks"]["authored_construction_reproduces"]["holds"] is True + drifted = run(m, StubEngine({"gov.cap": {2024: 951, 2025: 1071.09}})) + check = drifted["checks"]["authored_construction_reproduces"] + assert check["holds"] is False + assert check["differences"]["pe_baseline_modifier"]["authored_here"]["gov.cap"][ + "2025" + ] == round(646.35 * 1071.09 / 951, 2) + monkeypatch.setattr(premises.builder, "authored_construction", lambda *a: None) + assert ( + run(m, StubEngine({"gov.cap": 951}))["checks"][ + "authored_construction_reproduces" + ]["holds"] + is None + ) + + def broken(event, title, resolve): + raise TypeError("unsupported operand") + + monkeypatch.setattr(premises.builder, "authored_construction", broken) + assert ( + run(m, StubEngine({"gov.cap": None}))["checks"][ + "authored_construction_reproduces" + ]["holds"] + is None + ) + + +@settings(max_examples=100, deadline=None) +@given( + st.floats(allow_nan=True, allow_infinity=True), + st.integers(min_value=0, max_value=2**31), +) +def test_scalars_serialise_without_nan_or_infinity(value, seed): + out = premises.scalar(value) + json.dumps(out, allow_nan=False) + m = measure( + "forward_delta_on_certified_world", delta={"gov.x": {"2024-01-01": 1.0}} + ) + payload = premises.canonical_bytes( + {"m": run(m, StubEngine({"gov.x": value})), "seed": seed} + ) + assert b"NaN" not in payload and b"Infinity" not in payload + + +@settings(max_examples=50, deadline=None) +@given(st.integers(min_value=0, max_value=2**31)) +def test_output_is_independent_of_dictionary_order(seed): + rng = random.Random(seed) + paths = [f"gov.p{i}" for i in range(5)] + modifier = {p: {"2024-01-01": float(i)} for i, p in enumerate(paths)} + shuffled = list(modifier.items()) + rng.shuffle(shuffled) + law = {p: 10.0 for p in paths} + a = run(measure("reversal_on_certified_world", modifier=modifier), StubEngine(law)) + b = run( + measure("reversal_on_certified_world", modifier=dict(shuffled)), + StubEngine(law), + ) + assert premises.canonical_bytes(a) == premises.canonical_bytes(b) + + +def committed_engine(): + if importlib.util.find_spec("policyengine_uk") is None: + return None + from importlib.metadata import version + + return version("policyengine-uk") + + +@pytest.mark.parametrize( + "engine,path", + [ + ( + "2.125.1", + "results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/CONSTRUCTION_PREMISES.json", + ), + ("2.89.2", "results/uk/events/CONSTRUCTION_PREMISES.json"), + ], +) +def test_committed_premises_regenerate(engine, path): + if committed_engine() != engine: + pytest.skip(f"needs policyengine-uk {engine}") + payload = premises.canonical_bytes( + premises.audit(list(premises.EVENTS), premises.EngineView()) + ) + assert payload == (ROOT / path).read_bytes() diff --git a/tests/test_uk_event_version_scope.py b/tests/test_uk_event_version_scope.py new file mode 100644 index 0000000..8adc124 --- /dev/null +++ b/tests/test_uk_event_version_scope.py @@ -0,0 +1,246 @@ +"""Diagnoses and construction notes belong to the engine that established them.""" + +import copy +import json +from pathlib import Path + +from hypothesis import given, settings +from hypothesis import strategies as st + +from pipeline import compare_uk_event as compare +from pipeline import diagnose_uk_event_models as diagnose + +ROOT = Path(__file__).resolve().parent.parent +EVENTS = ( + "autumn_budget_2024", + "autumn_statement_2023", + "spring_budget_2024", + "spring_statement_2025", + "spring_budget_2023", +) +VARIANT = { + "engine_version": "2.125.1", + "policyengine_version": "6.2.6", + "data_bundle": "policyengine-uk-data-1.58.0", + "variant_id": "pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0", + "data_years": list(range(2024, 2031)), + "unsupported_years": [{"year": 2023, "reason": "before_data_year"}], + "run_manifest": None, +} +# Sentences that state 2.89.2 facts as facts about the run. +LEGACY_ONLY = ( + "£3.15", + "#1887", + "still has the 3% surcharge scale", + "−£786.808m", + "-£786.808m", + "processed employee NICs main rate is 8%", +) + + +def measures(): + for event in EVENTS: + registry = json.loads( + (ROOT / f"data/uk/events/{event}_measures.json").read_text() + ) + yield from registry["measures"] + + +def test_legacy_review_diagnoses_are_unchanged_and_others_are_prior(): + seen = 0 + for measure in measures(): + for year in (2024, 2027): + legacy = compare.review_diagnosis(measure["measure_key"], year) + assert legacy == compare.review_diagnosis( + measure["measure_key"], year, "", compare.LEGACY_ENGINE + ) + other = compare.review_diagnosis( + measure["measure_key"], year, "", "2.125.1" + ) + if not legacy: + assert other == {} + continue + seen += 1 + assert "cause_class" not in other and "issues" not in other + (prior,) = other["prior_diagnoses"] + assert prior["status"] == "not_reverified" + assert prior["established_on"] == ["2.89.2"] + assert { + k: v for k, v in prior.items() if k not in ("status", "established_on") + } == legacy + assert seen >= 9 + + +def test_construction_notes_on_another_engine_are_labelled_and_scoped(): + for measure in measures(): + legacy = compare.construction_note(measure) + assert legacy == compare.construction_note(measure, compare.LEGACY_ENGINE) + other = compare.construction_note(measure, "2.125.1") + if not measure.get("note"): + assert other == "" + continue + assert other.startswith("Authored against policyengine-uk 2.89.2: ") + assert other.endswith("recorded in CONSTRUCTION_PREMISES.json.") + appended = other.removeprefix("Authored against policyengine-uk 2.89.2: ") + appended = appended.removeprefix(measure["note"]) + assert not any(marker in appended for marker in LEGACY_ONLY) + + +def test_run_identity_defaults_to_the_legacy_run(tmp_path): + identity = compare.run_identity(tmp_path) + assert identity["engine_version"] == "2.89.2" + assert not compare.is_variant(identity) + (tmp_path / "RUN_MANIFEST.json").write_text( + json.dumps( + {"engine_version": "2.125.1", "variant_id": "v", "data_years": [2024]} + ) + ) + identity = compare.run_identity(tmp_path) + assert compare.is_variant(identity) + assert identity["run_manifest"] == tmp_path / "RUN_MANIFEST.json" + + +def test_variant_reports_state_their_execution_and_unsupported_years(): + registry = json.loads( + (ROOT / "data/uk/events/spring_budget_2023_measures.json").read_text() + ) + rows = [ + { + "event_slug": "spring_budget_2023", + "source_row_id": "x", + "measure_key": registry["measures"][0]["measure_key"], + "title": "t", + "classification": "partial", + "gap_kind": None, + "measure_type": "m", + "fy": "2023-24", + "year": 2023, + "tax_head": "Income tax", + "obr_value_gbp": 1.0, + "obr_value_gbp_decimal": "1", + "pe_value_gbp": None, + "pe_to_obr_ratio": None, + "ratio_bin": "pe_missing", + "status": "unsupported_on_variant", + "reason": "CY2023 is outside v's supported data years 2024–2030 (before_data_year)", + "diagnosis": compare.review_diagnosis( + "spring_budget_2023__pension_annual_allowance_package", + 2023, + "", + "2.125.1", + ), + "gap_gbp": None, + "explained_share": None, + } + ] + grid = { + "full_event_complete": False, + "full_supported_grid_complete": True, + "unsupported_years": [2023], + } + text = compare.render_markdown(registry, rows, grid, VARIANT) + assert "policyengine-uk 2.125.1 with policyengine 6.2.6" in text + assert "pinned certified 2023 population" not in text + assert "Unsupported on this release: calendar years 2023" in text + assert "1 source rows keep their OBR values" in text + assert "Numerical replay grid incomplete" not in text + assert "## Prior diagnoses, established on policyengine-uk 2.89.2" in text + assert "## Review diagnoses" not in text + legacy = compare.render_markdown(registry, [], None, None) + assert "pinned certified 2023 population" in legacy + + +def established(): + return json.loads((ROOT / "results/uk/events/MODEL_DIAGNOSTICS.json").read_text()) + + +def test_every_predicate_finds_its_defect_in_its_founding_observation(): + records = established() + assert {r["id"] for r in records} == set(diagnose.DEFECT_PREDICATES) + for record in records: + assert diagnose.defect_status(record) == "persists", record["id"] + + +def test_resolution_signatures(): + by_id = {r["id"]: r for r in established()} + fixed = { + "carers_allowance_earnings_test_absent": { + "annual_carers_allowance_gbp": [4331.0, 0.0] + }, + "cgt_main_rate_commencement": { + "2024-10-30": {"basic": 0.1346}, + "2025-04-06": {"basic": 0.18}, + }, + "sdlt_additional_home_hike_absent": { + "2025-04-06": [0.05, 0.07, 0.10, 0.15, 0.17] + }, + "private_school_vat_current_law_lever_zero": { + "2025-01-01": 0.2, + "2025-06-01": 0.2, + "2026-06-01": 0.2, + }, + "employer_nics_state_pension_age_exemption": { + "ni_class_1_employer": [6750.0, 6750.0] + }, + "pension_taper_omits_employer_contributions": { + "pension_annual_allowance": [45000.0] + }, + "annual_allowance_charge_single_marginal_rate": { + "personal_pension_contributions_tax": [6460.0] + }, + } + for identifier, observation in fixed.items(): + record = {**by_id[identifier], "observation": observation} + assert diagnose.defect_status(record) == "resolved", identifier + + +def test_moved_code_or_missing_variables_are_inconclusive_never_resolved(): + for record in established(): + missing_file = copy.deepcopy(record) + missing_file["evidence"] = {"engine_file": "x.py", "missing": True} + assert diagnose.defect_status(missing_file) == "inconclusive" + absent = {**record, "absent_variables": ["x"]} + assert diagnose.defect_status(absent) == "inconclusive" + + +observations = st.recursive( + st.none() + | st.booleans() + | st.floats(allow_nan=True) + | st.integers() + | st.text(max_size=4), + lambda children: ( + st.lists(children, max_size=3) + | st.dictionaries( + st.sampled_from( + [ + "2024-10-30", + "2025-04-06", + "2029", + "2030", + "basic", + "ni_class_1_employer", + "pension_annual_allowance", + "annual_carers_allowance_gbp", + "annual_synthetic_lcwra_element", + "2026-06-01", + "2027-06-01", + "lower_profits_limit", + "upper_profits_limit", + "2024-06-01", + "2025-06-01", + ] + ), + children, + max_size=4, + ) + ), + max_leaves=12, +) + + +@settings(max_examples=400, deadline=None) +@given(st.sampled_from(sorted(diagnose.DEFECT_PREDICATES)), observations) +def test_predicates_are_total(identifier, observation): + record = {"id": identifier, "evidence": {}, "observation": observation} + assert diagnose.defect_status(record) in diagnose.STATUSES From 35f61c683eb35f344a99120d3bc5197569e2d00e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 16:20:10 -0400 Subject: [PATCH 2/3] Diagnostics: a predicate never raises on a malformed observation CI's Hypothesis run found an observation where annual_carers_allowance_gbp is a dict: slicing it raises KeyError (slices are hashable from Python 3.12), which defect_status did not catch, so the predicate raised instead of reporting inconclusive. KeyError is now caught with the other lookup errors; 20,000 generated observations across all ten predicates return a status. Co-Authored-By: Claude Opus 5.5 --- pipeline/diagnose_uk_event_models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipeline/diagnose_uk_event_models.py b/pipeline/diagnose_uk_event_models.py index 75ba821..dcffc7a 100644 --- a/pipeline/diagnose_uk_event_models.py +++ b/pipeline/diagnose_uk_event_models.py @@ -660,7 +660,7 @@ def defect_status(result: dict) -> str: return "inconclusive" try: status = predicate(result.get("observation") or {}) - except (TypeError, ValueError, AttributeError, IndexError): + except (TypeError, ValueError, AttributeError, IndexError, KeyError): return "inconclusive" return status if status in STATUSES else "inconclusive" From ef48eb7dab44dc22f5ad328dc7224c2305edb82f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:01:01 -0400 Subject: [PATCH 3/3] Diagnostics: defect status is total over malformed records Review of 35f61c6 found one more escape: a JSON integer beyond float range (json.loads parses it exactly) meeting float arithmetic in _close or the LCWRA cutoff raised OverflowError. The caught set is now one named tuple, MALFORMED (TypeError, ValueError, AttributeError, LookupError, ArithmeticError), and the record-level lookups (id, evidence, supporting_evidence, absent_variables) sit inside the same guard, so a record of the wrong shape is inconclusive too. The totality property now generates every key a predicate reads (a new test reads the predicate source and checks this), integers beyond float range, and whole malformed records. 20,000 generated records return a status; dropping ArithmeticError makes the new huge-integer test fail. Co-Authored-By: Claude Opus 5.5 --- pipeline/diagnose_uk_event_models.py | 31 +++++--- tests/test_uk_event_version_scope.py | 104 +++++++++++++++++++++------ 2 files changed, 101 insertions(+), 34 deletions(-) diff --git a/pipeline/diagnose_uk_event_models.py b/pipeline/diagnose_uk_event_models.py index dcffc7a..edfdee9 100644 --- a/pipeline/diagnose_uk_event_models.py +++ b/pipeline/diagnose_uk_event_models.py @@ -644,23 +644,32 @@ def _annual_allowance_charge(o): } +# What a JSON value of the wrong shape can raise inside a predicate: a wrong +# type (TypeError, AttributeError), a short or long unpack (ValueError), an +# index or slice into the wrong container (LookupError), or an integer beyond +# float range meeting float arithmetic (ArithmeticError). +MALFORMED = (TypeError, ValueError, AttributeError, LookupError, ArithmeticError) + + def defect_status(result: dict) -> str: """persists | resolved | inconclusive, from the case's own observation. A missing engine file or variable is inconclusive: code that moved is not - evidence that the behaviour changed.""" - predicate = DEFECT_PREDICATES.get(result.get("id")) - if predicate is None or result.get("absent_variables"): - return "inconclusive" - evidence = [ - result.get("evidence") or {}, - *(result.get("supporting_evidence") or []), - ] - if any(item.get("missing") for item in evidence): - return "inconclusive" + evidence that the behaviour changed. Total over JSON records: a record or + observation of the wrong shape (a list where a dict was expected, an + integer too large for a float) is inconclusive, never an exception.""" try: + predicate = DEFECT_PREDICATES.get(result.get("id")) + if predicate is None or result.get("absent_variables"): + return "inconclusive" + evidence = [ + result.get("evidence") or {}, + *(result.get("supporting_evidence") or []), + ] + if any(item.get("missing") for item in evidence): + return "inconclusive" status = predicate(result.get("observation") or {}) - except (TypeError, ValueError, AttributeError, IndexError, KeyError): + except MALFORMED: return "inconclusive" return status if status in STATUSES else "inconclusive" diff --git a/tests/test_uk_event_version_scope.py b/tests/test_uk_event_version_scope.py index 8adc124..723b8fb 100644 --- a/tests/test_uk_event_version_scope.py +++ b/tests/test_uk_event_version_scope.py @@ -2,6 +2,7 @@ import copy import json +import re from pathlib import Path from hypothesis import given, settings @@ -203,37 +204,42 @@ def test_moved_code_or_missing_variables_are_inconclusive_never_resolved(): assert diagnose.defect_status(absent) == "inconclusive" +# Every key a predicate reads (test_property_keys_reach_every_predicate_input). +KEYS = [ + "2024-10-30", + "2025-04-06", + "2029", + "2030", + "basic", + "ni_class_1_employer", + "pension_annual_allowance", + "annual_carers_allowance_gbp", + "annual_synthetic_lcwra_element", + "2026-06-01", + "2027-06-01", + "lower_profits_limit", + "upper_profits_limit", + "2024-06-01", + "2025-06-01", + "2025-01-01", + "personal_pension_contributions_tax", + "uk_higher_threshold", + "uk_additional_threshold", + "dividend_higher_threshold", + "dividend_additional_threshold", +] + observations = st.recursive( st.none() | st.booleans() | st.floats(allow_nan=True) | st.integers() + # JSON integers parse exactly, so values beyond float range are legal. + | st.integers(min_value=10**308, max_value=10**400) | st.text(max_size=4), lambda children: ( st.lists(children, max_size=3) - | st.dictionaries( - st.sampled_from( - [ - "2024-10-30", - "2025-04-06", - "2029", - "2030", - "basic", - "ni_class_1_employer", - "pension_annual_allowance", - "annual_carers_allowance_gbp", - "annual_synthetic_lcwra_element", - "2026-06-01", - "2027-06-01", - "lower_profits_limit", - "upper_profits_limit", - "2024-06-01", - "2025-06-01", - ] - ), - children, - max_size=4, - ) + | st.dictionaries(st.sampled_from(KEYS), children, max_size=4) ), max_leaves=12, ) @@ -244,3 +250,55 @@ def test_moved_code_or_missing_variables_are_inconclusive_never_resolved(): def test_predicates_are_total(identifier, observation): record = {"id": identifier, "evidence": {}, "observation": observation} assert diagnose.defect_status(record) in diagnose.STATUSES + + +def test_property_keys_reach_every_predicate_input(): + """The generated keys cover every key a predicate reads, so the totality + property exercises each predicate past its first shape check.""" + source = Path(diagnose.__file__).read_text() + start = source.index("def _close(") + body = source[start : source.index("DEFECT_PREDICATES = {")] + read = set(re.findall(r'"([0-9a-z_-]+)"', body)) - set(diagnose.STATUSES) + generated = set(KEYS) + assert read <= generated, sorted(read - generated) + + +records = st.fixed_dictionaries( + {"id": st.sampled_from(sorted(diagnose.DEFECT_PREDICATES)) | observations}, + optional={ + "evidence": observations, + "supporting_evidence": observations, + "absent_variables": observations, + "observation": observations, + }, +) + + +@settings(max_examples=400, deadline=None) +@given(records) +def test_status_is_total_over_malformed_records(record): + assert diagnose.defect_status(record) in diagnose.STATUSES + + +def test_integers_beyond_float_range_are_inconclusive(): + huge = 10**400 + cases = { + "private_school_vat_current_law_lever_zero": { + "2025-01-01": 0, + "2025-06-01": huge, + "2026-06-01": 0, + }, + "cgt_main_rate_commencement": { + "2024-10-30": {"basic": huge}, + "2025-04-06": {"basic": 0}, + }, + "uc_lcwra_protection_stops_at_2030": { + "annual_synthetic_lcwra_element": {"2029": [huge], "2030": [0]} + }, + "employer_nics_state_pension_age_exemption": { + "ni_class_1_employer": [huge, 0.5] + }, + } + for identifier, observation in cases.items(): + record = {"id": identifier, "evidence": {}, "observation": observation} + assert diagnose.defect_status(record) == "inconclusive", identifier