From 8cad05e68868018023b2d8ad8f36492624369627 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 11:11:53 -0400 Subject: [PATCH 01/15] Record every comparable external estimate before an analysis is published MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the pre-publish comparables gate's scorecard side: the producer list by country, the gap threshold, one directory per analysis (sources/analysis--/) validated by scorecard_db/comparables.py, the published index policyengine-app-v2 reads, and an ingest that puts each record's claims and the analysis's own figures on the scorecard. First record: the £100,000 childcare dashboard, whose 30-hours cost is 46-50% below CenTax's static cost; the annotation cites microcosm#1126. Co-Authored-By: Claude Opus 5.5 --- data/comparables/index.json | 65 + data/comparables/policy.json | 8 + data/comparables/producers.json | 396 +++++ data/lanes.json | 24 +- scorecard_db/build_db.py | 11 + scorecard_db/comparables.py | 1377 +++++++++++++++++ scorecard_db/ingest_analysis_comparables.py | 294 ++++ .../analysis.json | 169 ++ .../annotations.json | 30 + .../claims_staged.jsonl | 20 + .../manifest.jsonl | 2 + .../search_log.json | 250 +++ tests/test_build_chain.py | 1 + 13 files changed, 2646 insertions(+), 1 deletion(-) create mode 100644 data/comparables/index.json create mode 100644 data/comparables/policy.json create mode 100644 data/comparables/producers.json create mode 100644 scorecard_db/comparables.py create mode 100644 scorecard_db/ingest_analysis_comparables.py create mode 100644 sources/analysis-uk-childcare-100k-threshold/analysis.json create mode 100644 sources/analysis-uk-childcare-100k-threshold/annotations.json create mode 100644 sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl create mode 100644 sources/analysis-uk-childcare-100k-threshold/manifest.jsonl create mode 100644 sources/analysis-uk-childcare-100k-threshold/search_log.json diff --git a/data/comparables/index.json b/data/comparables/index.json new file mode 100644 index 000000000..5014de718 --- /dev/null +++ b/data/comparables/index.json @@ -0,0 +1,65 @@ +{ + "_what": "Every PolicyEngine analysis whose comparables search is recorded in this repository, keyed by its sources/ directory. Generated by `python -m scorecard_db.comparables index` from the directories themselves; policyengine-app-v2 reads it to check a research entry's comparables record. A listing means the record passed the gate.", + "schema": "comparables/index/1", + "gap_threshold": 0.25, + "analyses": { + "analysis-uk-childcare-100k-threshold": { + "id": "uk/childcare-100k-threshold", + "kind": "app", + "country_id": "uk", + "slug": "childcare-100k-threshold", + "title": "Scrapping the childcare cliff edge", + "url": "https://policyengine.org/uk/childcare-100k-threshold", + "owner": "vahid-ahmadi", + "published": "2026-10-12", + "searches": [ + { + "date": "2026-10-21", + "kind": "backfill", + "producers_searched": [ + "academic", + "centax", + "centre_for_british_progress", + "dfe", + "dwp", + "fabian_society", + "hm_treasury", + "ifs", + "ippr", + "jrf", + "landman_economics", + "lse_case", + "niesr", + "obr", + "open_search", + "resolution_foundation", + "tax_policy_associates", + "uk_hmrc", + "ukmod" + ], + "producers_with_finds": [ + "centax", + "centre_for_british_progress" + ], + "found_not_staged": [ + "dfe", + "open_search" + ], + "not_applicable": [ + "scottish_fiscal_commission" + ], + "found": 20 + } + ], + "claims": 20, + "compared": 6, + "over_threshold": 6, + "explained": 6, + "pending": 0, + "unbounded_gaps": 0, + "max_abs_gap": 0.5, + "tracking_issues": [], + "search_log": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/sources/analysis-uk-childcare-100k-threshold/search_log.json" + } + } +} diff --git a/data/comparables/policy.json b/data/comparables/policy.json new file mode 100644 index 000000000..bc72c4f61 --- /dev/null +++ b/data/comparables/policy.json @@ -0,0 +1,8 @@ +{ + "_what": "Parameters of the pre-publish comparables gate (docs/COMPARABLES.md). Changing a value here changes what every analysis directory must explain; the validator and the published index read it, and nothing else defines it.", + "gap_threshold": 0.25, + "gap_rule": "relative gap = PolicyEngine value / external value in PolicyEngine's orientation - 1 (scorecard_db/signs.py orients the external value; an unsigned magnitude compares absolute values). A gap whose absolute value exceeds gap_threshold needs an annotation that names its cause and cites a basis before the analysis can be recorded as reviewed.", + "gap_threshold_status": "default proposed on 2026-10-21, queued for Max's ruling", + "reharvest_window_days": 183, + "reharvest_note": "Published analyses are searched again weekly for this many days after their registry date (the launchd job comparables-reharvest)." +} diff --git a/data/comparables/producers.json b/data/comparables/producers.json new file mode 100644 index 000000000..7c943b572 --- /dev/null +++ b/data/comparables/producers.json @@ -0,0 +1,396 @@ +{ + "_what": "The producers a pre-publish comparables search must cover, by country (docs/COMPARABLES.md). Every search logs every producer listed for the analysis's country whose `added` date is on or before the search date: found, none_found, or not_applicable when `applies_to` excludes the analysis's scope. `slug` is the claim `source` a staged claim from this producer carries; `aliases` are other claim-source slugs already in this repository that count as the same producer. The list was seeded from Max's list of 2026-10-21; entries with `why` were added beyond it, for the reason given.", + "scopes": { + "UK": ["uk_wide", "great_britain", "england", "scotland", "wales", "northern_ireland", "local"], + "US": ["federal", "state", "local"] + }, + "countries": { + "UK": [ + { + "slug": "obr", + "name": "Office for Budget Responsibility", + "kind": "official", + "url": "https://obr.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": ["obr_efo", "obr_pmd", "obr_policy_effects"], + "search_hints": ["site:obr.uk"], + "where_to_look": ["Economic and fiscal outlook policy costings and supplementary tables", "Policy measures database", "Briefing papers and working papers"] + }, + { + "slug": "hm_treasury", + "name": "HM Treasury", + "kind": "official", + "url": "https://www.gov.uk/government/organisations/hm-treasury", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:gov.uk \"HM Treasury\""], + "where_to_look": ["Budget and Spending Review policy costings documents", "Red book tables (Table 4.1 and the scorecard)", "Impact on households distributional analysis"] + }, + { + "slug": "uk_hmrc", + "name": "HM Revenue and Customs", + "kind": "official", + "url": "https://www.gov.uk/government/organisations/hm-revenue-customs", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:gov.uk \"tax information and impact note\""], + "where_to_look": ["Tax information and impact notes", "Direct effects of illustrative tax changes (ready reckoner)", "Statistics releases"], + "why": "HMRC's tax information and impact notes are the official costing and affected-population counts for every tax measure; a tax analysis without them misses the official comparator." + }, + { + "slug": "dwp", + "name": "Department for Work and Pensions", + "kind": "official", + "url": "https://www.gov.uk/government/organisations/department-for-work-pensions", + "added": "2026-10-21", + "applies_to": "all", + "aliases": ["dwp_takeup", "dwp_hbai", "dwp_pensions"], + "search_hints": ["site:gov.uk \"impact assessment\" DWP"], + "where_to_look": ["Impact assessments and equality analyses", "Benefit expenditure and caseload tables"], + "why": "DWP publishes the official impact assessments and costings for benefit measures, the counterpart of HMRC's notes for tax." + }, + { + "slug": "dfe", + "name": "Department for Education", + "kind": "official", + "url": "https://www.gov.uk/government/organisations/department-for-education", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:gov.uk \"Department for Education\"", "site:explore-education-statistics.service.gov.uk"], + "where_to_look": ["Impact assessments of childcare and education measures", "Explore education statistics (funded early education take-up)", "Answers to parliamentary questions and released FOI estimates"], + "why": "DfE runs the funded childcare entitlements and publishes or releases the official estimates of their cost; the first search (the £100,000 childcare dashboard, 2026-10-21) found DfE's estimate of the support the limit withholds, which no listed producer covered." + }, + { + "slug": "scottish_fiscal_commission", + "name": "Scottish Fiscal Commission", + "kind": "official", + "url": "https://fiscalcommission.scot/", + "added": "2026-10-21", + "applies_to": ["scotland"], + "aliases": [], + "search_hints": ["site:fiscalcommission.scot"], + "where_to_look": ["Scotland's Economic and Fiscal Forecasts policy costings"], + "why": "The official costing body for devolved Scottish tax and social security measures." + }, + { + "slug": "ifs", + "name": "Institute for Fiscal Studies", + "kind": "think_tank", + "url": "https://ifs.org.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:ifs.org.uk"], + "where_to_look": ["Reports, briefing notes and comments", "Budget and Green Budget analysis", "Working papers"] + }, + { + "slug": "resolution_foundation", + "name": "Resolution Foundation", + "kind": "think_tank", + "url": "https://www.resolutionfoundation.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:resolutionfoundation.org"], + "where_to_look": ["Reports, briefings and spotlights", "Budget responses"] + }, + { + "slug": "centax", + "name": "Centre for the Analysis of Taxation (CenTax)", + "kind": "think_tank", + "url": "https://centax.org.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:centax.org.uk"], + "where_to_look": ["Reports and policy briefings (HMRC administrative data)"] + }, + { + "slug": "centre_for_british_progress", + "name": "Centre for British Progress", + "kind": "think_tank", + "url": "https://www.britishprogress.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["\"Centre for British Progress\""], + "where_to_look": ["Reports and technical appendices"] + }, + { + "slug": "ippr", + "name": "Institute for Public Policy Research", + "kind": "think_tank", + "url": "https://www.ippr.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:ippr.org"], + "where_to_look": ["Research and briefings"] + }, + { + "slug": "fabian_society", + "name": "Fabian Society", + "kind": "think_tank", + "url": "https://fabians.org.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:fabians.org.uk"], + "where_to_look": ["Reports and policy papers"] + }, + { + "slug": "niesr", + "name": "National Institute of Economic and Social Research", + "kind": "think_tank", + "url": "https://www.niesr.ac.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:niesr.ac.uk"], + "where_to_look": ["Publications, including the living standards and household analyses"] + }, + { + "slug": "ukmod", + "name": "UKMOD (CeMPA, University of Essex)", + "kind": "model", + "url": "https://www.microsimulation.ac.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:microsimulation.ac.uk", "UKMOD CeMPA working paper"], + "where_to_look": ["CeMPA working papers and Budget briefs", "UKMOD country reports"] + }, + { + "slug": "lse_case", + "name": "Centre for Analysis of Social Exclusion (LSE CASE)", + "kind": "academic", + "url": "https://sticerd.lse.ac.uk/case/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:sticerd.lse.ac.uk/case", "LSE CASE paper"], + "where_to_look": ["CASE papers and reports"] + }, + { + "slug": "jrf", + "name": "Joseph Rowntree Foundation", + "kind": "think_tank", + "url": "https://www.jrf.org.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:jrf.org.uk"], + "where_to_look": ["Reports, analysis and Budget responses"] + }, + { + "slug": "landman_economics", + "name": "Landman Economics", + "kind": "model", + "url": "https://www.landmaneconomics.co.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["\"Landman Economics\"", "site:landmaneconomics.co.uk"], + "where_to_look": ["Commissioned reports (often published by IPPR, RF, JRF, NEF or unions)"] + }, + { + "slug": "tax_policy_associates", + "name": "Tax Policy Associates", + "kind": "think_tank", + "url": "https://taxpolicy.org.uk/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:taxpolicy.org.uk"], + "where_to_look": ["Reports and articles"] + }, + { + "slug": "academic", + "name": "Academic working papers", + "kind": "academic", + "url": "https://scholar.google.com/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["Google Scholar", "SSRN", "IZA discussion papers", "CEPR / VoxEU", "Institute for Social and Economic Research working papers"], + "where_to_look": ["Working papers and journal articles from the last five years that estimate the same quantity"] + }, + { + "slug": "open_search", + "name": "Open web search (any other producer)", + "kind": "open_search", + "url": "", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["the analysis's subject and search terms with cost, estimate, impact, households, poverty"], + "where_to_look": ["Any producer not listed above: other think tanks, charities, consultancies, unions, select committees, the Commons Library, press-reported estimates traced to their primary"], + "why": "A list never covers every producer; the Centre for British Progress estimate CenTax reviewed is the kind of comparable only an open search finds. A claim found here keeps its producer's own slug." + } + ], + "US": [ + { + "slug": "cbo", + "name": "Congressional Budget Office", + "kind": "official", + "url": "https://www.cbo.gov/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:cbo.gov"], + "where_to_look": ["Cost estimates", "Budget options", "Distributional analyses and reports"] + }, + { + "slug": "jct", + "name": "Joint Committee on Taxation", + "kind": "official", + "url": "https://www.jct.gov/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:jct.gov"], + "where_to_look": ["JCX revenue estimates", "Distributional tables", "Tax expenditure estimates"] + }, + { + "slug": "tpc", + "name": "Urban-Brookings Tax Policy Center", + "kind": "model", + "url": "https://taxpolicycenter.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:taxpolicycenter.org"], + "where_to_look": ["Model estimates tables (T-tables)", "Briefs and TaxVox posts"] + }, + { + "slug": "pwbm", + "name": "Penn Wharton Budget Model", + "kind": "model", + "url": "https://budgetmodel.wharton.upenn.edu/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:budgetmodel.wharton.upenn.edu"], + "where_to_look": ["Issue analyses and briefs"] + }, + { + "slug": "budget_lab", + "name": "The Budget Lab at Yale", + "kind": "model", + "url": "https://budgetlab.yale.edu/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:budgetlab.yale.edu"], + "where_to_look": ["Research reports and trackers"] + }, + { + "slug": "tax_foundation", + "name": "Tax Foundation", + "kind": "model", + "url": "https://taxfoundation.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:taxfoundation.org"], + "where_to_look": ["Research, including state tax analyses"] + }, + { + "slug": "urban", + "name": "Urban Institute", + "kind": "model", + "url": "https://www.urban.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": ["urban_sotsn"], + "search_hints": ["site:urban.org"], + "where_to_look": ["TRIM3 and ATTIS analyses", "Research reports and features"] + }, + { + "slug": "cbpp", + "name": "Center on Budget and Policy Priorities", + "kind": "think_tank", + "url": "https://www.cbpp.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:cbpp.org"], + "where_to_look": ["Reports and analyses"] + }, + { + "slug": "itep", + "name": "Institute on Taxation and Economic Policy", + "kind": "model", + "url": "https://itep.org/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:itep.org"], + "where_to_look": ["Federal and state analyses (ITEP microsimulation model)"] + }, + { + "slug": "cpsp", + "name": "Center on Poverty and Social Policy, Columbia University", + "kind": "academic", + "url": "https://www.povertycenter.columbia.edu/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["site:povertycenter.columbia.edu"], + "where_to_look": ["Poverty and Social Policy briefs", "Monthly poverty data"], + "why": "The most frequent independent estimator of the poverty effects of federal and state credits, which PolicyEngine's US analyses headline." + }, + { + "slug": "crfb", + "name": "Committee for a Responsible Federal Budget", + "kind": "think_tank", + "url": "https://www.crfb.org/", + "added": "2026-10-21", + "applies_to": ["federal"], + "aliases": [], + "search_hints": ["site:crfb.org"], + "where_to_look": ["Analyses and bill cost tallies"], + "why": "Publishes its own costings of federal proposals, often before CBO." + }, + { + "slug": "state_fiscal_note", + "name": "State fiscal notes and legislative fiscal offices", + "kind": "fiscal_note", + "url": "", + "added": "2026-10-21", + "applies_to": ["state", "local"], + "aliases": [], + "alias_pattern": "^[a-z]{2}_fiscal_note$", + "search_hints": [" legislature fiscal note ", " legislative fiscal office", " department of revenue estimate"], + "where_to_look": ["The bill's fiscal note on the legislature's site", "The state's legislative fiscal office, revenue department and budget office"] + }, + { + "slug": "academic", + "name": "Academic working papers", + "kind": "academic", + "url": "https://scholar.google.com/", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["Google Scholar", "NBER working papers", "SSRN"], + "where_to_look": ["Working papers and journal articles from the last five years that estimate the same quantity"] + }, + { + "slug": "open_search", + "name": "Open web search (any other producer)", + "kind": "open_search", + "url": "", + "added": "2026-10-21", + "applies_to": "all", + "aliases": [], + "search_hints": ["the analysis's subject and search terms with cost, estimate, impact, households, poverty"], + "where_to_look": ["Any producer not listed above: other think tanks, state budget offices, advocacy groups, press-reported estimates traced to their primary"], + "why": "A list never covers every producer. A claim found here keeps its producer's own slug." + } + ] + } +} diff --git a/data/lanes.json b/data/lanes.json index 0857fa242..efb8da0ae 100644 --- a/data/lanes.json +++ b/data/lanes.json @@ -4,7 +4,7 @@ "country": "US | UK | BE | NZ \u2014 the model instance the lane feeds (issue #42)", "note": "Repo-committed status feed (issue #7); graduate to Supabase when lane count makes commits noisy. Every lane writes its transition here." }, - "updated": "2026-10-19", + "updated": "2026-10-21", "lanes": [ { "id": "urban-sotsn", @@ -577,6 +577,28 @@ "running": false, "updated": "2026-10-15", "note": "88 of 132 claims computed \u2014 State individual income tax collections (T40) for 41 States, DC, NH and WA, FY2023-FY2025; FY2023 (a us-6.2.1 calibration target) and FY2025 compared with PE's CY2024 State income tax on us-6.2.1; adapter QC checks the US column against the 50 States on all 25 items; admin outturns staged as claims by decision on #176" + }, + { + "id": "analysis-comparables-uk", + "source": "Pre-publish comparables", + "area": "every external estimate of each PolicyEngine analysis, found before it is published", + "mode": 2, + "country": "UK", + "stage": "computed", + "running": false, + "updated": "2026-10-21", + "note": "1 analyses searched, 20 claims, 6 compared with the analysis's own estimate" + }, + { + "id": "analysis-comparables-us", + "source": "Pre-publish comparables", + "area": "every external estimate of each PolicyEngine analysis, found before it is published", + "mode": 2, + "country": "US", + "stage": "registered", + "running": false, + "updated": "2026-10-21", + "note": "0 analyses searched, 0 claims, 0 compared with the analysis's own estimate" } ] } diff --git a/scorecard_db/build_db.py b/scorecard_db/build_db.py index 163acba3b..57155d77c 100644 --- a/scorecard_db/build_db.py +++ b/scorecard_db/build_db.py @@ -39,6 +39,7 @@ uk_policy_effects OBR published economic effects of policy produce_uk + campaign_uk archive-resolved UK reckoner attaches nz_budget_scores New Zealand Treasury Budget score tables (official-only) + analysis_comparables pre-publish comparables records (sources/analysis-*) be_pit_reform Belgian PIT-reform claims + Axiom result attachments be_jrc JRC EUROMOD-BE model claims + honest demo attachments; final so its 2026-08-21 lane update cannot be regressed by @@ -60,6 +61,7 @@ from nz import ingest_official_budget_scores from . import ( + ingest_analysis_comparables, ingest_aspe_welfare_indicators, ingest_campaign, ingest_census_stc, @@ -286,6 +288,15 @@ def build(db_path: Path) -> dict: # computed PE result reads "computed" rather than "ingested" (#136). ("uk_ab2025_lanes", lambda: ingest_uk_ab2025.advance_lanes(db_path)), ("nz_budget_scores", lambda: ingest_official_budget_scores.ingest(db_path)), + # Pre-publish comparables (docs/COMPARABLES.md): every external + # estimate found for each PolicyEngine analysis, held out, with the + # analysis's own published figure as its result. Validates every + # sources/analysis-* directory first, so an incomplete record fails + # the build. + ( + "analysis_comparables", + lambda: ingest_analysis_comparables.ingest(db_path), + ), ("be_pit_reform", lambda: ingest_pit_reform_2026.ingest(db_path)), ("be_jrc", lambda: ingest_jrc_country_report.ingest(db_path)), ] diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py new file mode 100644 index 000000000..6d681501e --- /dev/null +++ b/scorecard_db/comparables.py @@ -0,0 +1,1377 @@ +"""Pre-publish comparables: every PolicyEngine analysis is searched against +every external producer before it is published (Max, 2026-10-21; docs/COMPARABLES.md). + +One directory per analysis, ``sources/analysis--/``: + + analysis.json the analysis: its registry entry on policyengine.org, + scope, search terms, the policy worlds it scores and + the PolicyEngine estimates it publishes, each pinned + to the results file it was read from (SHA-256) + search_log.json every search: date, kind, who ran it, and one entry + per producer in data/comparables/producers.json + (found / none_found / not_applicable, the queries run + and the pages checked) + manifest.jsonl one line per primary publication, hashed + claims_staged.jsonl every comparable found, staged under the harvest + rules (sources/harvest-uk-ab2025-2026-09-24/README.md): + verbatim value, registered metric and unit or a + proposal, named baseline, attribution, + benchmark_class; plus the PolicyEngine estimate it is + compared with, or why there is none + annotations.json the cause of every gap over the threshold + (data/comparables/policy.json), each citing a basis + +The validator below is the gate: a directory that fails it fails CI, so +everything on main is a complete record. ``build_index`` writes +data/comparables/index.json, which policyengine-app-v2's CI reads to check +that every research entry published after its cutover names a complete +record here. Nothing in this module reads the network. + + PYTHONPATH=. python -m scorecard_db.comparables check + PYTHONPATH=. python -m scorecard_db.comparables index [--check] + PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk \\ + --kind app --slug childcare-100k-threshold + PYTHONPATH=. python -m scorecard_db.comparables record \\ + analysis-uk-childcare-100k-threshold + PYTHONPATH=. python -m scorecard_db.comparables due \\ + --apps apps.json --posts posts.json --today 2026-10-26 + PYTHONPATH=. python -m scorecard_db.comparables report \\ + analysis-uk-childcare-100k-threshold --since 2026-10-21 +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import math +import re +import sys +from dataclasses import dataclass, field +from pathlib import Path + +from . import signs +from .models import ComparisonStatus, Metric, TimeBasis, UnitConcept + +REPO = Path(__file__).resolve().parent.parent +SOURCES = REPO / "sources" +COMPARABLES = REPO / "data" / "comparables" +POLICY_PATH = COMPARABLES / "policy.json" +PRODUCERS_PATH = COMPARABLES / "producers.json" +INDEX_PATH = COMPARABLES / "index.json" +REPO_URL = "https://github.com/PolicyEngine/policyengine-scorecard" + +ANALYSIS_SCHEMA = "comparables/analysis/1" +SEARCH_SCHEMA = "comparables/search/1" +INDEX_SCHEMA = "comparables/index/1" +DIR_PREFIX = "analysis-" + +SEARCH_KINDS = ("prepublish", "backfill", "reharvest") +# found_not_staged: a comparable exists but its primary cannot be read (harvest +# rule 8: a figure whose primary could not be read is never a claim); the note +# says what it is and where it was seen. +RESULTS = ("found", "none_found", "found_not_staged", "not_applicable") +VALUE_KINDS = ( + "point", + "central", + "range_low", + "range_high", + "approx_chart_reading", + "cumulative", +) +ATTRIBUTIONS = ("own", "restated", "same_assumptions") +BENCHMARK_CLASSES = ("different_model", "administrative_fact", "same_assumptions") +PARSE_CONFIDENCE = ("high", "medium", "low") +BASIS_KINDS = ("issue", "document", "engine_metadata", "diagnostic") +# An annotation's status is the comparison status its claims carry. "pending" +# is the one extra: a comparable a post-publication re-harvest found, whose +# gap is still being explained on its tracking issue. +ANNOTATION_STATUSES = ( + ComparisonStatus.COMPARABLE.value, + ComparisonStatus.CONSTRUCTED.value, + ComparisonStatus.CONCEPT_MISMATCH.value, + ComparisonStatus.PE_GAP.value, + "pending", +) +BASELINE_WORLD = "baseline" +# Searches that may find a producer not on the list; the claim then names +# that producer's own slug. +OPEN_FINDERS = ("open_search", "academic") + +_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +_DATETIME = re.compile( + r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$" +) +_SLUG = re.compile(r"^[a-z0-9]+(?:[-_][a-z0-9]+)*$") +_KEY = re.compile(r"^[a-z0-9]+(?:_[a-z0-9]+)*$") +_REGISTRY_SLUG = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") +_SHA256 = re.compile(r"^[0-9a-f]{64}$") +_COMMIT = re.compile(r"^[0-9a-f]{40}$") +_REPO = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +_GITHUB_ISSUE = re.compile( + r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(issues|pull)/\d+(#.*)?$" +) +_GITHUB_PINNED = re.compile( + r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(blob|tree)/[0-9a-f]{40}/\S+$" +) +_URL = re.compile(r"^https?://\S+$") + +# Harvest rule 1: ``value`` is ``value_raw`` normalised to the unit, with the +# conversion named. Only these conversions are checked mechanically; a new one +# is added here deliberately, never improvised in a staged row. +NORMALIZATIONS = { + "as printed": 1.0, + "percent as printed": 1.0, + "fraction x 100 -> percent": 100.0, + "percent / 100 -> fraction": 0.01, + "thousands x 1e3": 1e3, + "millions x 1e6": 1e6, + "billions x 1e9": 1e9, + "GBP k x 1e3 -> GBP": 1e3, + "GBP m x 1e6 -> GBP": 1e6, + "GBP bn x 1e9 -> GBP": 1e9, + "USD k x 1e3 -> USD": 1e3, + "USD m x 1e6 -> USD": 1e6, + "USD bn x 1e9 -> USD": 1e9, + "USD tn x 1e12 -> USD": 1e12, +} + +_NUMBER = re.compile(r"\d+(?:,\d{3})*(?:\.\d+)?|\.\d+") +_MINUS = ("-", "−", "–") + + +def parse_printed(value_raw: str) -> float: + """The number a printed string carries, without its unit or scale. + + "£0.80bn" -> 0.8, "-800m" -> -800, "13,360" -> 13360, "(0.4)" -> -0.4, + "−£1.2 billion" -> -1.2, "2.1 per cent" -> 2.1. Exactly one number must + appear; a string with two ("£1.2bn to £1.5bn") is two claims (range_low, + range_high), never one. + """ + if not isinstance(value_raw, str) or not value_raw.strip(): + raise ValueError(f"value_raw must be a nonempty string: {value_raw!r}") + text = value_raw.strip() + numbers = _NUMBER.findall(text) + if len(numbers) != 1: + raise ValueError( + f"value_raw {value_raw!r} carries {len(numbers)} numbers; stage one " + "number per claim (a range is two claims)" + ) + magnitude = float(numbers[0].replace(",", "")) + before = text[: text.index(numbers[0])] + negative = any(m in before for m in _MINUS) or ( + text.startswith("(") and text.rstrip().endswith(")") + ) + if "minus" in before.lower(): + negative = True + return -magnitude if negative else magnitude + + +def check_value(row: dict, where: str, signed: bool = False) -> list[str]: + """Harvest rule 1 for one row: the value parses back from what was printed. + + A staged claim is never re-signed (its sign convention travels in + conditions). A PolicyEngine estimate is stated in PolicyEngine's own + orientation for its metric (signs.ORIENTATION), so ``signed`` rows may + carry ``sign: -1`` with a ``sign_basis`` when the results file prints the + quantity the other way round (a cost as a positive number).""" + errors = [] + sign = row.get("sign", 1) + if signed: + if sign not in (1, -1): + return [f"{where}: sign must be 1 or -1"] + if sign == -1 and not _nonempty_str(row.get("sign_basis"), 10): + errors.append(f"{where}: sign -1 needs sign_basis saying why") + elif "sign" in row: + return [f"{where}: a staged claim is never re-signed (harvest rule 1)"] + normalization = row.get("normalization") + if normalization not in NORMALIZATIONS: + return [ + ( + f"{where}: normalization {normalization!r} is not registered " + f"(scorecard_db/comparables.py NORMALIZATIONS: {sorted(NORMALIZATIONS)})" + ) + ] + value = row.get("value") + if not isinstance(value, (int, float)) or isinstance(value, bool): + return [f"{where}: value must be a number, got {value!r}"] + if not math.isfinite(value): + return [f"{where}: value must be finite, got {value!r}"] + try: + parsed = parse_printed(row.get("value_raw")) + except ValueError as exc: + return [f"{where}: {exc}"] + expected = sign * parsed * NORMALIZATIONS[normalization] + if not math.isclose(value, expected, rel_tol=1e-9, abs_tol=1e-9): + errors.append( + f"{where}: value {value!r} is not value_raw {row.get('value_raw')!r} " + f"under {normalization!r}{' with sign -1' if sign == -1 else ''} " + f"(expected {expected!r})" + ) + return errors + + +# --- the gap ----------------------------------------------------------------- + + +def orientation(claim: dict, country: str) -> tuple[str, str]: + """How the published value points relative to PolicyEngine's value for the + metric: scorecard_db/signs.py decides it from the stated sign convention, + never from comparing the two signs.""" + return signs.classify( + { + "metric": claim.get("metric"), + "conditions": claim.get("conditions") or {}, + "country": country, + "source": claim.get("source"), + } + ) + + +def relative_gap(pe_value: float, external_value: float, relation: str) -> float | None: + """PolicyEngine / external - 1, with the external value in PolicyEngine's + orientation. None when no rule orients the external value. + + A zero external value gives 0 when PolicyEngine is also zero and +inf + otherwise, so a nonzero estimate against a published zero is always over + any threshold. Opposite signs give a gap below -1.""" + if relation == signs.SAME: + pe, ext = pe_value, external_value + elif relation == signs.OPPOSITE: + pe, ext = pe_value, -external_value + elif relation == signs.MAGNITUDE: + pe, ext = abs(pe_value), abs(external_value) + else: + return None + if ext == 0: + return 0.0 if pe == 0 else math.inf + return pe / ext - 1.0 + + +def over_threshold(gap: float | None, threshold: float) -> bool: + return gap is not None and abs(gap) > threshold + + +# --- loading ----------------------------------------------------------------- + + +def load_policy(path: Path = POLICY_PATH) -> dict: + policy = json.loads(path.read_text()) + threshold = policy.get("gap_threshold") + if not (isinstance(threshold, (int, float)) and 0 < threshold < 10): + raise ValueError( + f"{path}: gap_threshold must be a positive number: {threshold!r}" + ) + return policy + + +def load_producers(path: Path = PRODUCERS_PATH) -> dict: + data = json.loads(path.read_text()) + for country, producers in data["countries"].items(): + slugs = [p["slug"] for p in producers] + if len(slugs) != len(set(slugs)): + raise ValueError(f"{path}: duplicate producer slugs for {country}") + for p in producers: + applies = p["applies_to"] + if applies != "all" and not set(applies) <= set(data["scopes"][country]): + raise ValueError( + f"{path}: {p['slug']} applies_to unknown scope {applies}" + ) + if not _DATE.match(p["added"]): + raise ValueError(f"{path}: {p['slug']} added must be YYYY-MM-DD") + return data + + +def required_producers(producers: dict, country: str, on: str) -> list[dict]: + """Every producer a search on date ``on`` must log, in list order.""" + return [p for p in producers["countries"][country] if p["added"] <= on] + + +def applies(producer: dict, scope: str) -> bool: + return producer["applies_to"] == "all" or scope in producer["applies_to"] + + +def producer_for_source(producers: dict, country: str, source: str) -> str | None: + """The producer slug a claim's ``source`` belongs to, or None when the + source is not on the list (allowed only for open_search/academic finds).""" + for p in producers["countries"][country]: + if source == p["slug"] or source in p.get("aliases", []): + return p["slug"] + pattern = p.get("alias_pattern") + if pattern and re.match(pattern, source): + return p["slug"] + return None + + +def _read_jsonl(path: Path) -> list[dict]: + rows = [] + for n, line in enumerate(path.read_text().splitlines(), 1): + if not line.strip(): + continue + try: + rows.append(json.loads(line)) + except json.JSONDecodeError as exc: + raise ValueError(f"{path}:{n}: {exc}") from exc + return rows + + +@dataclass +class AnalysisRecord: + directory: Path + analysis: dict + search_log: dict + manifest: list[dict] + claims: list[dict] + annotations: list[dict] + load_errors: list[str] = field(default_factory=list) + + @property + def name(self) -> str: + return self.directory.name + + +FILES = ( + "analysis.json", + "search_log.json", + "manifest.jsonl", + "claims_staged.jsonl", + "annotations.json", +) + + +def load_analysis(directory: Path) -> AnalysisRecord: + errors: list[str] = [] + parts: dict = {} + for name in FILES: + path = directory / name + if not path.exists(): + errors.append(f"{directory.name}: missing {name}") + continue + try: + parts[name] = ( + _read_jsonl(path) + if name.endswith(".jsonl") + else json.loads(path.read_text()) + ) + except (ValueError, json.JSONDecodeError) as exc: + errors.append(f"{directory.name}: {name} does not parse: {exc}") + return AnalysisRecord( + directory=directory, + analysis=parts.get("analysis.json") or {}, + search_log=parts.get("search_log.json") or {}, + manifest=parts.get("manifest.jsonl") or [], + claims=parts.get("claims_staged.jsonl") or [], + annotations=parts.get("annotations.json") or [], + load_errors=errors, + ) + + +def analysis_dirs(root: Path = SOURCES) -> list[Path]: + return sorted( + p for p in root.iterdir() if p.is_dir() and p.name.startswith(DIR_PREFIX) + ) + + +# --- validation -------------------------------------------------------------- + + +def _registered(enum, value) -> bool: + return value in {m.value for m in enum} + + +def _string_map(value) -> bool: + return isinstance(value, dict) and all( + isinstance(k, str) and isinstance(v, str) for k, v in value.items() + ) + + +def _nonempty_str(value, minimum: int = 1) -> bool: + return isinstance(value, str) and len(value.strip()) >= minimum + + +def _baseline_labels() -> set[str]: + from .baselines import BASELINES + + return {label for _, label, *_ in BASELINES} + + +def _check_basis(basis: dict, where: str) -> list[str]: + kind, ref = basis.get("kind"), basis.get("ref") + if kind not in BASIS_KINDS: + return [f"{where}: basis kind {kind!r} not in {BASIS_KINDS}"] + if not _nonempty_str(ref): + return [f"{where}: basis ref is empty"] + if kind == "issue": + if not _GITHUB_ISSUE.match(ref): + return [f"{where}: an issue basis is a GitHub issue or pull URL: {ref!r}"] + return [] + if kind == "engine_metadata": + if _GITHUB_PINNED.match(ref) or (REPO / ref.split("#")[0]).exists(): + return [] + return [ + ( + f"{where}: engine metadata is a file pinned to a commit " + f"(github.com///blob/<40-hex>/) or a path in this repo: {ref!r}" + ) + ] + # document / diagnostic: a URL, or a path in this repository that exists + if _URL.match(ref): + return [] + if (REPO / ref.split("#")[0]).exists(): + return [] + return [f"{where}: {kind} basis {ref!r} is neither a URL nor a path in this repo"] + + +def validate( + record: AnalysisRecord, + producers: dict | None = None, + policy: dict | None = None, +) -> list[str]: + """Every reason this directory is not a complete comparables record.""" + producers = producers or load_producers() + policy = policy or load_policy() + errors = list(record.load_errors) + if errors: + return errors + name = record.name + a = record.analysis + + # analysis.json ----------------------------------------------------------- + if a.get("schema") != ANALYSIS_SCHEMA: + errors.append(f"{name}: analysis.json schema must be {ANALYSIS_SCHEMA!r}") + country = a.get("country") + if country not in producers["countries"]: + errors.append(f"{name}: country {country!r} has no producer list") + return errors + registry = a.get("registry") or {} + kind, slug = registry.get("kind"), registry.get("slug") + if kind not in ("app", "post"): + errors.append(f"{name}: registry.kind must be 'app' or 'post'") + if not (isinstance(slug, str) and _REGISTRY_SLUG.match(slug)): + errors.append(f"{name}: registry.slug {slug!r} is not a policyengine.org slug") + if registry.get("country_id") != country.lower(): + errors.append(f"{name}: registry.country_id must be {country.lower()!r}") + expected_dir = f"{DIR_PREFIX}{country.lower()}-{slug}" + if name != expected_dir: + errors.append(f"{name}: directory must be named {expected_dir!r}") + if a.get("id") != f"{country.lower()}/{slug}": + errors.append(f"{name}: id must be {country.lower()}/{slug}") + if not (isinstance(a.get("url"), str) and a["url"].startswith("https://")): + errors.append(f"{name}: url must be the analysis's https URL") + if not _nonempty_str(a.get("title")): + errors.append(f"{name}: title is empty") + scope = a.get("scope") + if scope not in producers["scopes"][country]: + errors.append(f"{name}: scope {scope!r} not in {producers['scopes'][country]}") + if not _nonempty_str(a.get("subject"), 10): + errors.append(f"{name}: subject must say what the analysis estimates") + terms = a.get("search_terms") + if not (isinstance(terms, list) and terms and all(_nonempty_str(t) for t in terms)): + errors.append(f"{name}: search_terms must be a nonempty list of strings") + owner = a.get("owner") or {} + if not ( + isinstance(owner.get("author"), str) and _REGISTRY_SLUG.match(owner["author"]) + ): + errors.append(f"{name}: owner.author must be the app-v2 authors.json key") + published = a.get("published") + if published is not None and not ( + isinstance(published, str) and _DATE.match(published) + ): + errors.append(f"{name}: published must be YYYY-MM-DD or null") + worlds = a.get("worlds") + if not isinstance(worlds, dict): + errors.append(f"{name}: worlds must map a world key to its policy slug") + worlds = {} + for wkey, world in worlds.items(): + if wkey == BASELINE_WORLD or not _KEY.match(wkey): + errors.append(f"{name}: world key {wkey!r} is reserved or malformed") + if not (isinstance(world, dict) and _SLUG.match(str(world.get("policy", "")))): + errors.append(f"{name}: world {wkey!r} needs a policy slug") + if not _nonempty_str((world or {}).get("description"), 10): + errors.append(f"{name}: world {wkey!r} needs a description") + world_keys = set(worlds) | {BASELINE_WORLD} + + estimates: dict[str, dict] = {} + for i, e in enumerate(a.get("pe_estimates") or []): + where = f"{name}: pe_estimates[{i}]" + key = e.get("key") + if not (isinstance(key, str) and _KEY.match(key)) or key in estimates: + errors.append(f"{where}: key {key!r} missing, malformed or repeated") + continue + estimates[key] = e + where = f"{name}: estimate {key}" + if not _nonempty_str(e.get("label")): + errors.append(f"{where}: label is empty") + if e.get("world") not in world_keys: + errors.append(f"{where}: world {e.get('world')!r} is not defined") + if not _registered(Metric, e.get("metric")): + errors.append(f"{where}: metric {e.get('metric')!r} is not registered") + if not _registered(UnitConcept, e.get("unit_concept")): + errors.append( + f"{where}: unit_concept {e.get('unit_concept')!r} is not registered" + ) + if not _registered(TimeBasis, e.get("time_basis")): + errors.append( + f"{where}: time_basis {e.get('time_basis')!r} is not registered" + ) + if not isinstance(e.get("period"), int) or isinstance(e.get("period"), bool): + errors.append(f"{where}: period must be an integer year") + if not _string_map(e.get("conditions", {})): + errors.append(f"{where}: conditions must map strings to strings") + errors.extend(check_value(e, where, signed=True)) + prov = e.get("provenance") or {} + if not (isinstance(prov.get("repo"), str) and _REPO.match(prov["repo"])): + errors.append(f"{where}: provenance.repo must be /") + if not (isinstance(prov.get("commit"), str) and _COMMIT.match(prov["commit"])): + errors.append(f"{where}: provenance.commit must be a 40-hex commit") + if not (isinstance(prov.get("sha256"), str) and _SHA256.match(prov["sha256"])): + errors.append( + f"{where}: provenance.sha256 must be the results file's SHA-256" + ) + for f in ("path", "field", "dataset"): + if not _nonempty_str(prov.get(f)): + errors.append(f"{where}: provenance.{f} is empty") + if not ( + isinstance(prov.get("computed_at"), str) + and _DATETIME.match(prov["computed_at"]) + ): + errors.append( + f"{where}: provenance.computed_at must be an ISO date or time" + ) + engine = prov.get("engine") + if not (_string_map(engine) and engine): + errors.append( + f"{where}: provenance.engine must map package names to versions" + ) + if not estimates: + errors.append( + f"{name}: pe_estimates is empty; an analysis that publishes no estimate " + "is declared no_estimate in the registry, not recorded here" + ) + + # manifest ---------------------------------------------------------------- + manifest: dict[str, dict] = {} + for i, m in enumerate(record.manifest): + where = f"{name}: manifest.jsonl line {i + 1}" + if not (isinstance(m.get("sha256"), str) and _SHA256.match(m["sha256"])): + errors.append(f"{where}: sha256 must be 64 hex") + continue + if not (isinstance(m.get("url"), str) and _URL.match(m["url"])): + errors.append(f"{where}: url is not a URL") + for f in ("title", "publisher", "retrieved", "access"): + if not _nonempty_str(m.get(f)): + errors.append(f"{where}: {f} is empty") + if m["sha256"] in manifest: + errors.append(f"{where}: sha256 repeated") + manifest[m["sha256"]] = m + + # search_log ---------------------------------------------------------------- + log = record.search_log + if log.get("schema") != SEARCH_SCHEMA: + errors.append(f"{name}: search_log.json schema must be {SEARCH_SCHEMA!r}") + searches = log.get("searches") or [] + if not searches: + errors.append(f"{name}: search_log has no searches") + claim_found: dict[str, tuple[str, str, str]] = {} # key -> (date, kind, producer) + previous_date = "" + for i, s in enumerate(searches): + where = f"{name}: search {i + 1}" + date, skind = s.get("date"), s.get("kind") + if not (isinstance(date, str) and _DATE.match(date)): + errors.append(f"{where}: date must be YYYY-MM-DD") + continue + if date < previous_date: + errors.append(f"{where}: searches must be in date order") + previous_date = date + if skind not in SEARCH_KINDS: + errors.append(f"{where}: kind {skind!r} not in {SEARCH_KINDS}") + if i == 0 and skind == "reharvest": + errors.append( + f"{where}: the first search is a prepublish or backfill search" + ) + if not _nonempty_str(s.get("by")): + errors.append(f"{where}: by must name the session or Subfleet job") + entries = s.get("producers") or [] + logged = [p.get("producer") for p in entries] + required = required_producers(producers, country, date) + required_slugs = [p["slug"] for p in required] + missing = [p for p in required_slugs if p not in logged] + unknown = [p for p in logged if p not in required_slugs] + if missing: + errors.append(f"{where}: producers not searched: {missing}") + if unknown: + errors.append( + f"{where}: producers not on the {country} list on {date}: {unknown}" + ) + if len(logged) != len(set(logged)): + errors.append(f"{where}: a producer is logged twice") + by_slug = {p["slug"]: p for p in required} + for entry in entries: + slug_ = entry.get("producer") + pwhere = f"{where} {slug_}" + result = entry.get("result") + claims = entry.get("claims") or [] + queries = entry.get("queries") or [] + checked = entry.get("checked") or [] + if result not in RESULTS: + errors.append(f"{pwhere}: result {result!r} not in {RESULTS}") + continue + if not all(_nonempty_str(q) for q in queries): + errors.append(f"{pwhere}: queries must be strings") + if not all(isinstance(u, str) and _URL.match(u) for u in checked): + errors.append(f"{pwhere}: checked must be URLs") + producer = by_slug.get(slug_) + if result == "not_applicable": + if producer is not None and applies(producer, scope): + errors.append( + f"{pwhere}: not_applicable, but this producer applies to " + f"{scope!r} analyses; search it" + ) + if claims: + errors.append(f"{pwhere}: not_applicable with claims") + continue + if not (queries or checked): + errors.append( + f"{pwhere}: {result} with no query run and no page checked" + ) + if result == "none_found" and claims: + errors.append(f"{pwhere}: none_found with claims") + if result == "found_not_staged": + if claims: + errors.append(f"{pwhere}: found_not_staged with claims") + if not checked: + errors.append( + f"{pwhere}: found_not_staged lists the pages it was seen on" + ) + if not _nonempty_str(entry.get("note"), 20): + errors.append( + f"{pwhere}: found_not_staged says what was found and why it is not staged" + ) + if result == "found" and not claims: + errors.append(f"{pwhere}: found with no claims") + for key in claims: + if key in claim_found: + errors.append( + f"{pwhere}: claim {key} already found by an earlier search" + ) + claim_found[key] = (date, skind, slug_) + + # claims ---------------------------------------------------------------------- + labels = _baseline_labels() + claims_by_key: dict[str, dict] = {} + for i, c in enumerate(record.claims): + key = c.get("key") + where = f"{name}: claim {key or i + 1}" + if not (isinstance(key, str) and _KEY.match(key)) or key in claims_by_key: + errors.append(f"{where}: key missing, malformed or repeated") + continue + claims_by_key[key] = c + found = claim_found.get(key) + if found is None: + errors.append(f"{where}: no search entry lists this claim") + else: + if c.get("found_on") != found[0]: + errors.append( + f"{where}: found_on must be {found[0]}, the search that found it" + ) + if c.get("found_by") != found[2]: + errors.append( + f"{where}: found_by must be {found[2]}, the producer logged" + ) + source = c.get("source") + found_by = c.get("found_by") + if not (isinstance(source, str) and _SLUG.match(source)): + errors.append(f"{where}: source must be a producer slug") + elif source == "open_search": + errors.append( + f"{where}: source names the producer that published it, not open_search" + ) + elif found_by not in OPEN_FINDERS: + # a listed producer's own search finds only that producer's claims + belongs = producer_for_source(producers, country, source) + if belongs != found_by: + errors.append( + f"{where}: source {source!r} is not {found_by}'s; a claim from " + "another producer is logged under that producer, or under " + "open_search when the producer is not on the list" + ) + metric = c.get("metric") + if metric is None: + if not _nonempty_str(c.get("proposed_metric")): + errors.append(f"{where}: metric null needs proposed_metric") + elif not _registered(Metric, metric): + errors.append( + f"{where}: metric {metric!r} is not registered; use proposed_metric" + ) + unit = c.get("unit_concept") + if unit is None: + if not _nonempty_str(c.get("proposed_unit")): + errors.append(f"{where}: unit_concept null needs proposed_unit") + elif not _registered(UnitConcept, unit): + errors.append( + f"{where}: unit_concept {unit!r} is not registered; use proposed_unit" + ) + if c.get("value_kind") not in VALUE_KINDS: + errors.append( + f"{where}: value_kind {c.get('value_kind')!r} not in {VALUE_KINDS}" + ) + if c.get("value_kind") == "approx_chart_reading": + if not _nonempty_str(c.get("note"), 10): + errors.append(f"{where}: a chart reading says how it was read in note") + else: + errors.extend(check_value(c, where)) + quote = re.sub(r"[\s,]", "", str(c.get("quote", ""))) + raw_digits = "".join(_NUMBER.findall(str(c.get("value_raw", "")))).replace( + ",", "" + ) + if raw_digits and raw_digits not in quote: + errors.append( + f"{where}: quote does not contain the printed value {c.get('value_raw')!r}" + ) + if not isinstance(c.get("period"), int) or isinstance(c.get("period"), bool): + errors.append(f"{where}: period must be an integer year (FY end year)") + if not _registered(TimeBasis, c.get("time_basis")): + errors.append( + f"{where}: time_basis {c.get('time_basis')!r} is not registered" + ) + if not _string_map(c.get("conditions")): + errors.append(f"{where}: conditions must map strings to strings") + world = c.get("world") + if world not in world_keys: + errors.append(f"{where}: world {world!r} is not defined in analysis.json") + if world != BASELINE_WORLD and not _nonempty_str(c.get("reform_hint")): + errors.append( + f"{where}: reform_hint must quote the producer's own words for the world" + ) + if c.get("attribution") not in ATTRIBUTIONS: + errors.append( + f"{where}: attribution {c.get('attribution')!r} not in {ATTRIBUTIONS}" + ) + bclass = c.get("benchmark_class") + if bclass not in BENCHMARK_CLASSES: + errors.append( + f"{where}: benchmark_class {bclass!r} not in {BENCHMARK_CLASSES}" + ) + if (c.get("attribution") == "same_assumptions") != ( + bclass == "same_assumptions" + ): + errors.append( + f"{where}: same_assumptions attribution and benchmark_class go together" + ) + if bclass == "administrative_fact" and world == BASELINE_WORLD: + errors.append( + f"{where}: an administrative level is calibration material; stage it " + "under a producer-level source, where relationships.py assigns it" + ) + baseline = c.get("baseline_policy") + if baseline is not None and baseline not in labels: + errors.append( + f"{where}: baseline_policy {baseline!r} is not registered in " + "scorecard_db/baselines.py (harvest rule 7: register it first)" + ) + if "proposed_baseline" in c: + errors.append( + f"{where}: register the baseline before staging (harvest rule 7)" + ) + pub = c.get("publication") or {} + for f in ("title", "url", "date", "publisher", "locator", "access"): + if not _nonempty_str(pub.get(f)): + errors.append(f"{where}: publication.{f} is empty") + sha = pub.get("primary_sha256") + if sha not in manifest: + errors.append( + f"{where}: publication.primary_sha256 is not in manifest.jsonl" + ) + elif manifest[sha].get("url") != pub.get("url"): + errors.append(f"{where}: publication.url differs from its manifest line") + for f in ("source_table", "source_column", "quote"): + if not _nonempty_str(c.get(f)): + errors.append(f"{where}: {f} is empty") + if c.get("parse_confidence") not in PARSE_CONFIDENCE: + errors.append(f"{where}: parse_confidence not in {PARSE_CONFIDENCE}") + if c.get("status") != "ok": + errors.append(f"{where}: status must be 'ok'") + compare = c.get("compare_to") + if compare is None: + if not _nonempty_str(c.get("pe_missing"), 10): + errors.append(f"{where}: not compared, so pe_missing must say why") + continue + if c.get("pe_missing") is not None: + errors.append(f"{where}: compared, so pe_missing must be absent") + est = estimates.get((compare or {}).get("pe_estimate")) + if est is None: + errors.append(f"{where}: compare_to names no PolicyEngine estimate") + continue + if c.get("attribution") != "own": + errors.append( + f"{where}: only a producer's own estimate is compared ({c.get('attribution')})" + ) + if c.get("world") != est.get("world"): + across = (compare or {}).get("across_worlds") + if not isinstance(across, dict): + errors.append( + f"{where}: world {c.get('world')!r} differs from {est['key']}'s " + f"{est.get('world')!r}; compare across worlds only with " + "compare_to.across_worlds giving the reason and its basis" + ) + else: + if not _nonempty_str(across.get("reason"), 20): + errors.append( + f"{where}: across_worlds.reason must say why the quantities match" + ) + bases = across.get("basis") + if not (isinstance(bases, list) and bases): + errors.append(f"{where}: across_worlds.basis must cite a source") + else: + for b in bases: + errors.extend( + _check_basis(b if isinstance(b, dict) else {}, where) + ) + for f in ("metric", "unit_concept", "period", "time_basis"): + if c.get(f) != est.get(f): + errors.append( + f"{where}: {f} {c.get(f)!r} differs from {est['key']}'s {est.get(f)!r}; " + "a different quantity is not compared (pe_missing says so)" + ) + relation, basis = orientation(c, country) + if relation == signs.UNKNOWN: + errors.append( + f"{where}: the sign convention does not orient the value ({basis}); " + "state it in conditions.sign_convention in words signs.py reads" + ) + + for key in claim_found: + if key not in claims_by_key: + errors.append(f"{name}: search lists claim {key}, which is not staged") + + # annotations ------------------------------------------------------------------- + annotated: dict[str, dict] = {} + ids = set() + for i, n in enumerate(record.annotations): + nid = n.get("id") + where = f"{name}: annotation {nid or i + 1}" + if not (isinstance(nid, str) and _SLUG.match(nid)) or nid in ids: + errors.append(f"{where}: id missing, malformed or repeated") + continue + ids.add(nid) + status = n.get("status") + if status not in ANNOTATION_STATUSES: + errors.append(f"{where}: status {status!r} not in {ANNOTATION_STATUSES}") + if not _nonempty_str(n.get("cause"), 20): + errors.append(f"{where}: cause must state the cause (20+ characters)") + bases = n.get("basis") + if not (isinstance(bases, list) and bases): + errors.append( + f"{where}: basis must cite at least one source (no fabricated mechanisms)" + ) + else: + for b in bases: + errors.extend(_check_basis(b if isinstance(b, dict) else {}, where)) + if not isinstance(n.get("explains_gap"), bool): + errors.append(f"{where}: explains_gap must be true or false") + if status == "pending": + if n.get("explains_gap"): + errors.append( + f"{where}: a pending annotation does not explain the gap yet" + ) + if not ( + isinstance(n.get("tracking_issue"), str) + and _GITHUB_ISSUE.match(n["tracking_issue"]) + ): + errors.append( + f"{where}: pending needs the tracking_issue opened for the owner" + ) + keys = n.get("claims") + if not (isinstance(keys, list) and keys): + errors.append(f"{where}: claims must list the claim keys it covers") + continue + for key in keys: + if key not in claims_by_key: + errors.append(f"{where}: claim {key} is not staged") + elif key in annotated: + errors.append( + f"{where}: claim {key} already has a status from {annotated[key]['id']}" + ) + else: + annotated[key] = n + if ( + status == "pending" + and claim_found.get(key, ("", ""))[1] != "reharvest" + ): + errors.append( + f"{where}: {key} was found before publication; its gap is " + "explained before the analysis is recorded, not left pending" + ) + + # the gate ------------------------------------------------------------------------ + if errors: + return errors + for row in evaluate(record, policy): + if not row["over"]: + continue + note = annotated.get(row["key"]) + if note is None: + errors.append( + f"{name}: claim {row['key']} is {row['gap']:+.1%} from {row['estimate']} " + f"(threshold {policy['gap_threshold']:.0%}) and no annotation explains it" + ) + elif note["status"] != "pending" and not note.get("explains_gap"): + errors.append( + f"{name}: claim {row['key']} is over the threshold; its annotation " + f"{note['id']} must explain the gap (explains_gap: true)" + ) + return errors + + +def evaluate(record: AnalysisRecord, policy: dict | None = None) -> list[dict]: + """One row per staged claim: the comparison and its gap. Assumes a valid + record (``validate`` returns no errors).""" + policy = policy or load_policy() + threshold = policy["gap_threshold"] + country = record.analysis["country"] + estimates = {e["key"]: e for e in record.analysis["pe_estimates"]} + annotated = {} + for n in record.annotations: + for key in n["claims"]: + annotated[key] = n + rows = [] + for c in record.claims: + note = annotated.get(c["key"]) + row = { + "key": c["key"], + "source": c["source"], + "found_on": c["found_on"], + "estimate": None, + "relation": None, + "gap": None, + "over": False, + "status": note["status"] if note else None, + "annotation": note["id"] if note else None, + "explains_gap": bool(note and note.get("explains_gap")), + "tracking_issue": (note or {}).get("tracking_issue"), + } + compare = c.get("compare_to") + if compare: + est = estimates[compare["pe_estimate"]] + relation, _ = orientation(c, country) + gap = relative_gap(est["value"], c["value"], relation) + # a comparison across two policy worlds is a documented + # construction, never plainly comparable + default = ( + ComparisonStatus.CONSTRUCTED.value + if c["world"] != est["world"] + else ComparisonStatus.COMPARABLE.value + ) + row.update( + estimate=est["key"], + relation=relation, + gap=gap, + over=over_threshold(gap, threshold), + status=row["status"] or default, + ) + rows.append(row) + return rows + + +# --- the index ------------------------------------------------------------------ + + +def _search_summary(search: dict) -> dict: + by_result: dict[str, list[str]] = {r: [] for r in RESULTS} + for p in search["producers"]: + by_result[p["result"]].append(p["producer"]) + searched = sorted( + p["producer"] for p in search["producers"] if p["result"] != "not_applicable" + ) + return { + "date": search["date"], + "kind": search["kind"], + "producers_searched": searched, + "producers_with_finds": sorted(by_result["found"]), + "found_not_staged": sorted(by_result["found_not_staged"]), + "not_applicable": sorted(by_result["not_applicable"]), + "found": sum(len(p.get("claims") or []) for p in search["producers"]), + } + + +def _round(x: float | None) -> float | None: + if x is None: + return None + if math.isinf(x): + return None + return round(x, 4) + + +def index_entry(record: AnalysisRecord, policy: dict) -> dict: + a = record.analysis + rows = evaluate(record, policy) + compared = [r for r in rows if r["estimate"]] + gaps = [abs(r["gap"]) for r in compared if r["gap"] is not None] + over = [r for r in compared if r["over"]] + return { + "id": a["id"], + "kind": a["registry"]["kind"], + "country_id": a["registry"]["country_id"], + "slug": a["registry"]["slug"], + "title": a["title"], + "url": a["url"], + "owner": a["owner"]["author"], + "published": a.get("published"), + "searches": [_search_summary(s) for s in record.search_log["searches"]], + "claims": len(rows), + "compared": len(compared), + "over_threshold": len(over), + "explained": sum(1 for r in over if r["explains_gap"]), + "pending": sum(1 for r in rows if r["status"] == "pending"), + "unbounded_gaps": sum(1 for g in gaps if math.isinf(g)), + "max_abs_gap": _round( + max((g for g in gaps if not math.isinf(g)), default=None) + ), + "tracking_issues": sorted( + {r["tracking_issue"] for r in rows if r["tracking_issue"]} + ), + "search_log": f"{REPO_URL}/blob/main/sources/{record.name}/search_log.json", + } + + +def build_index( + root: Path = SOURCES, producers: dict | None = None, policy: dict | None = None +) -> dict: + """The published index. Raises if any analysis directory is incomplete: + the index lists only complete records, so a listing is the proof.""" + producers = producers or load_producers() + policy = policy or load_policy() + analyses = {} + problems = [] + for directory in analysis_dirs(root): + record = load_analysis(directory) + errors = validate(record, producers, policy) + if errors: + problems.extend(errors) + continue + analyses[directory.name] = index_entry(record, policy) + if problems: + raise ValueError("incomplete comparables records:\n " + "\n ".join(problems)) + return { + "_what": ( + "Every PolicyEngine analysis whose comparables search is recorded in " + "this repository, keyed by its sources/ directory. Generated by " + "`python -m scorecard_db.comparables index` from the directories " + "themselves; policyengine-app-v2 reads it to check a research entry's " + "comparables record. A listing means the record passed the gate." + ), + "schema": INDEX_SCHEMA, + "gap_threshold": policy["gap_threshold"], + "analyses": analyses, + } + + +def render_index(index: dict) -> str: + return json.dumps(index, indent=1, sort_keys=False, ensure_ascii=False) + "\n" + + +# --- the app-v2 record ------------------------------------------------------------ + + +def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: + """The ``comparables`` block for the analysis's apps.json / posts.json entry.""" + searches = [ + s + for s in record.search_log["searches"] + if s["kind"] in ("prepublish", "backfill") + ] + if search_date: + searches = [s for s in searches if s["date"] == search_date] + if not searches: + raise ValueError( + f"{record.name}: no prepublish or backfill search on {search_date}" + ) + summary = _search_summary(searches[-1]) + return { + "status": "reviewed", + "scorecardSource": record.name, + "searchDate": summary["date"], + "searchKind": summary["kind"], + "producersSearched": summary["producers_searched"], + "found": summary["found"], + "searchLog": f"{REPO_URL}/blob/main/sources/{record.name}/search_log.json", + } + + +# --- scaffolding ----------------------------------------------------------------- + + +def scaffold( + country: str, kind: str, slug: str, today: str, root: Path = SOURCES +) -> Path: + """A skeleton directory for a new search, with every producer to log.""" + producers = load_producers() + country = country.upper() + directory = root / f"{DIR_PREFIX}{country.lower()}-{slug}" + if directory.exists(): + raise SystemExit( + f"{directory} exists; add a search to its search_log.json instead" + ) + directory.mkdir(parents=True) + analysis = { + "schema": ANALYSIS_SCHEMA, + "id": f"{country.lower()}/{slug}", + "country": country, + "registry": {"kind": kind, "slug": slug, "country_id": country.lower()}, + "title": "", + "url": f"https://policyengine.org/{country.lower()}/{'research/' if kind == 'post' else ''}{slug}", + "published": None, + "owner": {"author": ""}, + "scope": "", + "subject": "", + "search_terms": [], + "worlds": {}, + "pe_estimates": [], + } + log = { + "schema": SEARCH_SCHEMA, + "searches": [ + { + "date": today, + "kind": "prepublish", + "by": "", + "producers": [ + { + "producer": p["slug"], + "result": "", + "queries": [], + "checked": [], + "claims": [], + "note": "", + } + for p in required_producers(producers, country, today) + ], + } + ], + } + (directory / "analysis.json").write_text( + json.dumps(analysis, indent=1, ensure_ascii=False) + "\n" + ) + (directory / "search_log.json").write_text( + json.dumps(log, indent=1, ensure_ascii=False) + "\n" + ) + (directory / "manifest.jsonl").write_text("") + (directory / "claims_staged.jsonl").write_text("") + (directory / "annotations.json").write_text("[]\n") + return directory + + +# --- re-harvest support ---------------------------------------------------------- + + +def _post_slug(filename: str) -> str: + # website/src/data/posts/postTransformers.ts + return filename.rsplit(".", 1)[0].lower().replace("_", "-") + + +def research_entries(apps: list[dict], posts: list[dict]) -> list[dict]: + """policyengine.org research entries with a date, as {kind, country_id, + slug, date, title, authors, tags}. A post can carry several country tags; + it is listed once per country it is tagged with.""" + out = [] + for app in apps: + if not app.get("displayWithResearch") or not app.get("date"): + continue + out.append( + { + "kind": "app", + "country_id": app["countryId"], + "slug": app["slug"], + "date": app["date"][:10], + "title": app.get("title", ""), + "authors": app.get("authors", []), + "tags": app.get("tags", []), + "comparables": app.get("comparables"), + } + ) + for post in posts: + for country_id in [ + t for t in post.get("tags", []) if t in ("uk", "us", "ca", "ng", "il") + ]: + out.append( + { + "kind": "post", + "country_id": country_id, + "slug": _post_slug(post["filename"]), + "date": post["date"][:10], + "title": post.get("title", ""), + "authors": post.get("authors", []), + "tags": post.get("tags", []), + "comparables": post.get("comparables"), + } + ) + return out + + +def due( + apps: list[dict], + posts: list[dict], + today: str, + window_days: int, + root: Path = SOURCES, +) -> list[dict]: + """Research entries published within the re-harvest window that publish an + estimate (a policy tag, or a reviewed comparables record), with the + directory that holds (or will hold) their record.""" + start = (dt.date.fromisoformat(today) - dt.timedelta(days=window_days)).isoformat() + rows = [] + for e in research_entries(apps, posts): + if not (start <= e["date"] <= today): + continue + record = e.get("comparables") or {} + if record.get("status") == "no_estimate": + continue + if "policy" not in e["tags"] and record.get("status") != "reviewed": + continue + name = f"{DIR_PREFIX}{e['country_id']}-{e['slug']}" + rows.append( + { + **{k: v for k, v in e.items() if k != "comparables"}, + "source": name, + "recorded": (root / name).is_dir(), + } + ) + return sorted(rows, key=lambda r: (r["date"], r["source"])) + + +def report(record: AnalysisRecord, since: str, policy: dict | None = None) -> dict: + """New comparables found on or after ``since`` and every gap over the + threshold among them: the text of a re-harvest's issue for the owner.""" + rows = [r for r in evaluate(record, policy) if r["found_on"] >= since] + return { + "source": record.name, + "since": since, + "new_claims": [r["key"] for r in rows], + "over_threshold": [ + { + k: r[k] + for k in ( + "key", + "source", + "estimate", + "gap", + "status", + "annotation", + "tracking_issue", + ) + } + for r in rows + if r["over"] + ], + } + + +# --- CLI ------------------------------------------------------------------------------ + + +def _today() -> str: + return dt.datetime.now(dt.timezone.utc).date().isoformat() + + +def _main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(prog="python -m scorecard_db.comparables") + sub = parser.add_subparsers(dest="cmd", required=True) + p_check = sub.add_parser("check", help="validate analysis directories") + p_check.add_argument( + "dirs", nargs="*", help="directories (default: every sources/analysis-*)" + ) + p_index = sub.add_parser("index", help="write data/comparables/index.json") + p_index.add_argument( + "--check", action="store_true", help="fail if the committed index is stale" + ) + p_scaffold = sub.add_parser("scaffold", help="start a record for a new analysis") + p_scaffold.add_argument("--country", required=True) + p_scaffold.add_argument("--kind", required=True, choices=("app", "post")) + p_scaffold.add_argument("--slug", required=True) + p_scaffold.add_argument("--today", default=_today()) + p_record = sub.add_parser("record", help="print the app-v2 comparables block") + p_record.add_argument("source") + p_record.add_argument("--search-date") + p_due = sub.add_parser("due", help="analyses due for re-harvest") + p_due.add_argument("--apps", required=True) + p_due.add_argument("--posts", required=True) + p_due.add_argument("--today", default=_today()) + p_report = sub.add_parser("report", help="comparables found since a date") + p_report.add_argument("source") + p_report.add_argument("--since", required=True) + args = parser.parse_args(argv) + + if args.cmd == "check": + dirs = [Path(d) for d in args.dirs] or analysis_dirs() + producers, policy = load_producers(), load_policy() + failed = 0 + for d in dirs: + errors = validate(load_analysis(d.resolve()), producers, policy) + if errors: + failed += 1 + print(f"FAIL {d.name}") + for e in errors: + print(f" {e}") + else: + print(f"ok {d.name}") + return 1 if failed else 0 + if args.cmd == "index": + text = render_index(build_index()) + if args.check: + if not INDEX_PATH.exists() or INDEX_PATH.read_text() != text: + print( + f"{INDEX_PATH} is stale: run python -m scorecard_db.comparables index" + ) + return 1 + print(f"{INDEX_PATH} is current") + return 0 + INDEX_PATH.write_text(text) + print(f"wrote {INDEX_PATH}") + return 0 + if args.cmd == "scaffold": + print(scaffold(args.country, args.kind, args.slug, args.today)) + return 0 + if args.cmd == "record": + record = load_analysis(SOURCES / args.source) + errors = validate(record) + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + print( + json.dumps( + app_record(record, args.search_date), indent=2, ensure_ascii=False + ) + ) + return 0 + if args.cmd == "due": + apps = json.loads(Path(args.apps).read_text()) + posts = json.loads(Path(args.posts).read_text()) + window = load_policy()["reharvest_window_days"] + print( + json.dumps( + due(apps, posts, args.today, window), indent=1, ensure_ascii=False + ) + ) + return 0 + if args.cmd == "report": + record = load_analysis(SOURCES / args.source) + errors = validate(record) + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + print( + json.dumps( + report(record, args.since), indent=1, ensure_ascii=False, default=str + ) + ) + return 0 + return 2 + + +def sha256_file(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +if __name__ == "__main__": + sys.exit(_main(sys.argv[1:])) diff --git a/scorecard_db/ingest_analysis_comparables.py b/scorecard_db/ingest_analysis_comparables.py new file mode 100644 index 000000000..cab898c30 --- /dev/null +++ b/scorecard_db/ingest_analysis_comparables.py @@ -0,0 +1,294 @@ +"""Ingest every pre-publish comparables record (docs/COMPARABLES.md). + +Each ``sources/analysis--/`` directory records the external +estimates found for one PolicyEngine analysis and the analysis's own +published estimates. This ingest puts them on the scorecard: + +- every staged claim the database can hold becomes a claim, held out + (another producer's estimate of the analysis's world is a comparator, + never a calibration target: Max, 2026-08-02), with + ``conditions.analysis`` naming the analysis so its claims are retrievable + together and never collide with a producer-level source staging the same + publication; +- every compared claim gets one PolicyEngine result: the analysis's own + published figure, read from the results file analysis.json pins, with the + comparison status its annotation gives (``comparable`` when none does); +- a claim with no PolicyEngine counterpart is published without a result, + and ``conditions.pe_missing`` says why. + +Rows the database cannot hold are tallied, not dropped silently: a claim +with a proposed metric or unit (harvest rule 2) waits for its vocabulary +decision, and a restated government figure is dropped under the #86 rule +(the producer did not compute it). Both stay in the directory and in the +published index. + +Same contract as every ingest here: the directory is validated first +(scorecard_db/comparables.py), the replacement is one transaction keyed on +``publication.registry``, and an invalid directory fails the build. + + PYTHONPATH=. python -m scorecard_db.ingest_analysis_comparables data/scorecard.db +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from . import comparables as cmp +from .baselines import BASELINES +from .db import LANE_SQL, RESULTS_SQL, SCORES_SQL, ScorecardDB +from .harvest import REPO, finish, policy_ref, with_baseline_condition +from .models import ( + CURRENT_LAW_DESCRIPTOR, + ComparisonStatus, + ExternalScore, + Metric, + PEResult, + ReformRef, + TimeBasis, + UnitConcept, + baseline_key, +) + +REGISTRY = "analysis_comparables" +# One mission-control lane per country: the app files a lane under one country. +LANES = { + "UK": "analysis-comparables-uk", + "US": "analysis-comparables-us", +} +LANE_FEED_META = { + lane: { + "source": "Pre-publish comparables", + "area": "every external estimate of each PolicyEngine analysis, found before it is published", + "mode": 2, + "country": country, + } + for country, lane in LANES.items() +} +FIRST_SEARCH_DATE = "2026-10-21" + + +def _baseline_descriptor(label: str | None) -> dict | None: + if label is None: + return None + for descriptor, registered, *_ in BASELINES: + if registered == label: + return None if descriptor == CURRENT_LAW_DESCRIPTOR else descriptor + raise ValueError(f"baseline {label!r} is not registered") + + +def _reform( + record: cmp.AnalysisRecord, world: str, baseline_label: str | None +) -> ReformRef: + if world == cmp.BASELINE_WORLD: + return ReformRef() + policy = record.analysis["worlds"][world]["policy"] + return policy_ref(policy, baseline=_baseline_descriptor(baseline_label)) + + +def _engine_version(estimate: dict) -> str: + engine = estimate["provenance"]["engine"] + return " ".join(f"{name}=={version}" for name, version in sorted(engine.items())) + + +def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult], dict]: + producers, policy = cmp.load_producers(), cmp.load_policy() + scores: list[ExternalScore] = [] + results: list[PEResult] = [] + tally = { + country: { + "analyses": 0, + "claims": 0, + "results": 0, + "not_compared": 0, + "skipped": {}, + } + for country in LANES + } + for directory in cmp.analysis_dirs(root): + record = cmp.load_analysis(directory) + errors = cmp.validate(record, producers, policy) + if errors: + raise ValueError( + f"{directory.name} is not a complete record:\n " + "\n ".join(errors) + ) + a = record.analysis + if a["country"] not in LANES: + raise ValueError( + f"{directory.name}: no mission-control lane for {a['country']}" + ) + t = tally[a["country"]] + t["analyses"] += 1 + estimates = {e["key"]: e for e in a["pe_estimates"]} + evaluation = {row["key"]: row for row in cmp.evaluate(record, policy)} + notes = {} + for note in record.annotations: + for key in note["claims"]: + notes[key] = note + for c in record.claims: + if c["attribution"] == "restated": + t["skipped"]["restated"] = t["skipped"].get("restated", 0) + 1 + continue + if c.get("metric") is None or c.get("unit_concept") is None: + t["skipped"]["proposed_vocabulary"] = ( + t["skipped"].get("proposed_vocabulary", 0) + 1 + ) + continue + reform = _reform(record, c["world"], c.get("baseline_policy")) + conditions = { + **c["conditions"], + "analysis": a["id"], + "analysis_claim": c["key"], + "benchmark_class": c["benchmark_class"], + "value_verbatim": c["value_raw"], + } + if c["value_kind"] != "point": + conditions["estimate_kind"] = c["value_kind"] + with_baseline_condition(conditions, reform) + if not c.get("compare_to"): + conditions["pe_missing"] = c["pe_missing"] + publication = { + **{ + k: c["publication"][k] + for k in ("title", "url", "date", "publisher", "locator") + }, + "primary_sha256": c["publication"]["primary_sha256"], + "table": c["source_table"], + "quote": c["quote"], + "registry": REGISTRY, + "country": a["country"], + "analysis_url": a["url"], + } + if not c.get("compare_to"): + publication["publish_without_result"] = True + score = ExternalScore( + source=c["source"], + metric=Metric(c["metric"]), + unit_concept=UnitConcept(c["unit_concept"]), + period=c["period"], + time_basis=TimeBasis(c["time_basis"]), + value=float(c["value"]), + conditions=conditions, + reform=reform, + calibration_relationship="held_out", + source_model=c.get("source_model"), + source_column=c["source_column"], + publication=publication, + value_kind=c["unit_concept"], + status="ok", + ) + scores.append(score) + t["claims"] += 1 + row = evaluation[c["key"]] + if not row["estimate"]: + t["not_compared"] += 1 + continue + est = estimates[row["estimate"]] + note = notes.get(c["key"]) + status = row["status"] + annotations = [] + if note: + annotations.append(f"{note['id']}: {note['cause']}") + if status == "pending": + # the comparison is like for like; its gap is open on the owner's issue + status = ComparisonStatus.COMPARABLE.value + annotations.append( + f"gap unexplained, tracked at {note['tracking_issue']}" + ) + world_baseline = a["worlds"].get(est["world"], {}).get("baseline") + executed = _baseline_descriptor(world_baseline) or CURRENT_LAW_DESCRIPTOR + prov = est["provenance"] + results.append( + PEResult( + claim_id=score.claim_id(), + computed_value=float(est["value"]), + status=ComparisonStatus(status), + engine_version=_engine_version(est), + data_bundle=prov["dataset"], + pe_construction=json.dumps( + { + "analysis": a["id"], + "estimate": est["key"], + "repo": prov["repo"], + "commit": prov["commit"], + "path": prov["path"], + "sha256": prov["sha256"], + "field": prov["field"], + "relation": row["relation"], + }, + sort_keys=True, + ), + run_id=f"analysis:{a['id']}@{prov['commit'][:7]}", + computed_at=prov["computed_at"], + annotations=annotations, + baseline_key=baseline_key(executed), + ) + ) + t["results"] += 1 + return finish(scores, REGISTRY), results, tally + + +def ingest(db_path: Path) -> dict: + scores, results, tally = stage() + db = ScorecardDB(db_path) + from .baselines import register_baselines_txn + from .ingest_harvest import sync_lane_feed + + with db.conn: + existing = [ + r[0] + for r in db.conn.execute( + "SELECT claim_id FROM external_scores" + " WHERE json_extract(publication, '$.registry') = ?", + (REGISTRY,), + ) + ] + db.conn.executemany( + "DELETE FROM pe_results WHERE claim_id = ?", [(c,) for c in existing] + ) + db.conn.executemany( + "DELETE FROM external_scores WHERE claim_id = ?", [(c,) for c in existing] + ) + collisions = [ + s.claim_id() + for s in scores + if db.conn.execute( + "SELECT 1 FROM external_scores WHERE claim_id = ?", (s.claim_id(),) + ).fetchone() + ] + if collisions: + raise ValueError( + f"analysis claims collide with existing claims: {collisions[:3]}" + ) + db.conn.executemany(SCORES_SQL, [ScorecardDB.score_row(s) for s in scores]) + db.conn.executemany(RESULTS_SQL, [ScorecardDB.result_row(r) for r in results]) + register_baselines_txn(db) + updated = max( + ( + s["date"] + for d in cmp.analysis_dirs() + for s in cmp.load_analysis(d).search_log.get("searches", []) + ), + default=FIRST_SEARCH_DATE, + ) + for country, lane in LANES.items(): + t = tally[country] + stage_name = ( + "computed" + if t["results"] + else ("ingested" if t["claims"] else "registered") + ) + detail = ( + f"{t['analyses']} analyses searched, {t['claims']} claims, " + f"{t['results']} compared with the analysis's own estimate" + ) + db.conn.execute(LANE_SQL, (lane, stage_name, detail, updated)) + sync_lane_feed(db, REPO / "data" / "lanes.json", updated, lanes=LANE_FEED_META) + db.close() + return tally + + +if __name__ == "__main__": + out = Path(sys.argv[1] if len(sys.argv) > 1 else "data/scorecard.db") + print(json.dumps(ingest(out), indent=1)) diff --git a/sources/analysis-uk-childcare-100k-threshold/analysis.json b/sources/analysis-uk-childcare-100k-threshold/analysis.json new file mode 100644 index 000000000..cce20138e --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/analysis.json @@ -0,0 +1,169 @@ +{ + "schema": "comparables/analysis/1", + "id": "uk/childcare-100k-threshold", + "country": "UK", + "registry": { + "kind": "app", + "slug": "childcare-100k-threshold", + "country_id": "uk" + }, + "title": "Scrapping the childcare cliff edge", + "url": "https://policyengine.org/uk/childcare-100k-threshold", + "published": "2026-10-12", + "owner": { + "author": "vahid-ahmadi" + }, + "scope": "uk_wide", + "subject": "The cost of removing the £100,000 adjusted-net-income limit on the 30 funded hours for working parents (England) and on Tax-Free Childcare (UK-wide), who gains, and the cliff it removes.", + "search_terms": [ + "£100,000 childcare cliff edge", + "100k childcare limit cost", + "30 funded hours income limit", + "tax-free childcare £100,000 limit", + "abolish childcare cliff edge cost" + ], + "worlds": { + "both_limits_removed": { + "policy": "uk_free_childcare_hours_and_tfc_100k_limit_removed", + "description": "The dashboard's reform: the £100,000 limit removed from both the 30 funded hours for working parents and Tax-Free Childcare." + }, + "free_hours_limit_removed": { + "policy": "uk_free_childcare_hours_100k_limit_removed", + "description": "The £100,000 limit removed from the free hours for working parents only; Tax-Free Childcare keeps its limit (CenTax section 4.2.1)." + } + }, + "pe_estimates": [ + { + "key": "thirty_hours_static_fy2027", + "label": "Static cost of the 30 funded hours, both limits removed, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -434000000, + "value_raw": "0.434", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "thirty_hours_static_fy2029", + "label": "Static cost of the 30 funded hours, both limits removed, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -505000000, + "value_raw": "0.505", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_fy2027", + "label": "Static cost of both schemes, both limits removed, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -593000000, + "value_raw": "0.593", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_fy2029", + "label": "Static cost of both schemes, both limits removed, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -678000000, + "value_raw": "0.678", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + } + ] +} diff --git a/sources/analysis-uk-childcare-100k-threshold/annotations.json b/sources/analysis-uk-childcare-100k-threshold/annotations.json new file mode 100644 index 000000000..d71c1031c --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/annotations.json @@ -0,0 +1,30 @@ +[ + { + "id": "hours-draw-microcosm-1126", + "claims": [ + "centax_static_central_fy2027", + "centax_static_central_fy2029", + "centax_static_high_cost_fy2027", + "centax_static_high_cost_fy2029", + "centax_static_low_cost_fy2027", + "centax_static_low_cost_fy2029" + ], + "status": "constructed", + "explains_gap": true, + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/microcosm/issues/1126" + }, + { + "kind": "engine_metadata", + "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/blob/f25f0dbd46c31d336bde06898d801a1887a8cf89/data/results.json" + }, + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/pull/8" + } + ] + } +] diff --git a/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl new file mode 100644 index 000000000..40f554092 --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl @@ -0,0 +1,20 @@ +{"key": "centax_static_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -800000000, "value_raw": "-800m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Static Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -520000000, "value_raw": "-520m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_static_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -980000000, "value_raw": "-980m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Static Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -640000000, "value_raw": "-640m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_static_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -820000000, "value_raw": "-820m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -610000000, "value_raw": "-610m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_static_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -1010000000, "value_raw": "-1,010m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -770000000, "value_raw": "-770m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_static_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -580000000, "value_raw": "-580m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 140000000, "value_raw": "+140m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_static_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -700000000, "value_raw": "-700m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 160000000, "value_raw": "+160m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "cbp_net_table2_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 70000000, "value_raw": "0.070", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 77000000, "value_raw": "0.077", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 91000000, "value_raw": "0.091", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 132000000, "value_raw": "0.132", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 330000000, "value_raw": "0.330", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 349000000, "value_raw": "0.349", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 399000000, "value_raw": "0.399", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 427000000, "value_raw": "0.427", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} diff --git a/sources/analysis-uk-childcare-100k-threshold/manifest.jsonl b/sources/analysis-uk-childcare-100k-threshold/manifest.jsonl new file mode 100644 index 000000000..fbb6dfdaa --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/manifest.jsonl @@ -0,0 +1,2 @@ +{"url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "title": "Removing the childcare cliff-edge: impacts and cost of reform", "date": "2026-09-09", "publisher": "CenTax", "sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "bytes": 2709003, "doc_type": "pdf", "access": "direct", "retrieved": "2026-10-21", "note": "Same bytes as sources/centax-childcare-100k (scorecard#207)."} +{"url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "date": "2026-03-25", "publisher": "Centre for British Progress", "sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "bytes": 359068, "doc_type": "pdf", "access": "direct", "retrieved": "2026-10-21", "note": "Same bytes as sources/cbp-rates-and-wrongs (scorecard#207)."} diff --git a/sources/analysis-uk-childcare-100k-threshold/search_log.json b/sources/analysis-uk-childcare-100k-threshold/search_log.json new file mode 100644 index 000000000..2b9ab21c8 --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/search_log.json @@ -0,0 +1,250 @@ +{ + "schema": "comparables/search/1", + "searches": [ + { + "date": "2026-10-21", + "kind": "backfill", + "by": "session:local_4b58a325-2146-422b-80fa-69a117a812aa", + "producers": [ + { + "producer": "obr", + "result": "none_found", + "queries": [ + "site:obr.uk childcare £100,000 adjusted net income limit cost" + ], + "checked": [], + "claims": [], + "note": "No OBR costing: no government has announced the measure." + }, + { + "producer": "hm_treasury", + "result": "none_found", + "queries": [ + "site:gov.uk \"100,000\" childcare free hours tax-free childcare income limit impact assessment cost" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "uk_hmrc", + "result": "none_found", + "queries": [ + "HMRC tax-free childcare £100,000 income limit tax information and impact note removal cost", + "site:gov.uk \"100,000\" childcare free hours tax-free childcare income limit impact assessment cost" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "dwp", + "result": "none_found", + "queries": [ + "site:gov.uk \"100,000\" childcare free hours tax-free childcare income limit impact assessment cost" + ], + "checked": [], + "claims": [], + "note": "DWP administers neither scheme." + }, + { + "producer": "dfe", + "result": "found_not_staged", + "queries": [ + "Department for Education estimate cost of removing £100,000 income limit working parents childcare entitlement parliamentary question", + "site:gov.uk \"100,000\" childcare free hours tax-free childcare income limit impact assessment cost" + ], + "checked": [ + "https://media.quilter.com/search/2026/up-to-99000-children-now-miss-out-on-childcare-support-as-frozen-100000-earnings-threshold-locks-families-out/" + ], + "claims": [], + "note": "DfE estimates released to Quilter under FOI (Quilter, 5 August 2026): 50,500 to 99,000 children affected by the limit in 2025/26, with support worth £446m to £874m, assuming full take-up and maximum use. The FOI response is not published, so the figure has no primary to stage (harvest rule 8); it is also for 2025-26, a year the dashboard does not estimate." + }, + { + "producer": "scottish_fiscal_commission", + "result": "not_applicable", + "queries": [], + "checked": [], + "claims": [], + "note": "A UK-wide analysis of a reserved scheme and an England-only one." + }, + { + "producer": "ifs", + "result": "none_found", + "queries": [ + "IFS £100,000 childcare cliff edge free hours tax-free childcare cost", + "site:ifs.org.uk childcare cliff edge 100,000 free hours" + ], + "checked": [], + "claims": [], + "note": "Coverage cites IFS household examples of the cliff; no IFS costing of removing the limit was found." + }, + { + "producer": "resolution_foundation", + "result": "none_found", + "queries": [ + "Resolution Foundation childcare £100,000 income limit cost of removing", + "site:resolutionfoundation.org childcare 100,000 cliff edge 2026" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "centax", + "result": "found", + "queries": [ + "cost of removing £100,000 childcare cliff edge 30 hours estimate 2026" + ], + "checked": [ + "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "https://warwick.ac.uk/fac/soc/economics/news/2026/9/new_centax_report_explores_impact_of_100k_childcare_cliff_edge/" + ], + "claims": [ + "centax_static_central_fy2027", + "centax_net_central_fy2027", + "centax_static_central_fy2029", + "centax_net_central_fy2029", + "centax_static_high_cost_fy2027", + "centax_net_high_cost_fy2027", + "centax_static_high_cost_fy2029", + "centax_net_high_cost_fy2029", + "centax_static_low_cost_fy2027", + "centax_net_low_cost_fy2027", + "centax_static_low_cost_fy2029", + "centax_net_low_cost_fy2029" + ], + "note": "Table 4.2: three scenarios x two tax years; static and net staged, the behavioural margins and bunching counts are in sources/centax-childcare-100k." + }, + { + "producer": "centre_for_british_progress", + "result": "found", + "queries": [ + "Centre for British Progress \"Rates and Wrongs\" childcare cliff technical appendix" + ], + "checked": [ + "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "https://www.ajbell.co.uk/group/news/scrapping-ps100000-childcare-cliff-edge-could-raise-over-ps130m" + ], + "claims": [ + "cbp_net_table2_fy2026", + "cbp_net_table2_fy2027", + "cbp_net_table2_fy2028", + "cbp_net_table2_fy2029", + "cbp_net_table3_fy2026", + "cbp_net_table3_fy2027", + "cbp_net_table3_fy2028", + "cbp_net_table3_fy2029" + ], + "note": "" + }, + { + "producer": "ippr", + "result": "none_found", + "queries": [ + "site:ippr.org childcare 100,000 free hours higher earners" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "fabian_society", + "result": "none_found", + "queries": [ + "site:fabians.org.uk childcare £100,000 cliff edge" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "niesr", + "result": "none_found", + "queries": [ + "site:niesr.ac.uk childcare 100,000 income limit" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "ukmod", + "result": "none_found", + "queries": [ + "UKMOD CeMPA childcare entitlement £100,000 threshold simulation" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "lse_case", + "result": "none_found", + "queries": [ + "LSE CASE childcare entitlement working parents high income threshold analysis" + ], + "checked": [ + "https://www.lse.ac.uk/research-shows-how-childcare-support-could-be-improved-without-substantial-government-cost" + ], + "claims": [], + "note": "CASE and the Early Education and Childcare Coalition (Paull, 19 October 2026) model a redesign of the subsidies; the release gives no cost of removing the £100,000 limit." + }, + { + "producer": "jrf", + "result": "none_found", + "queries": [ + "site:jrf.org.uk childcare free hours higher earners £100,000" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "landman_economics", + "result": "none_found", + "queries": [ + "Landman Economics childcare free hours income threshold" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "tax_policy_associates", + "result": "none_found", + "queries": [ + "site:taxpolicy.org.uk childcare 100,000 cliff" + ], + "checked": [], + "claims": [], + "note": "" + }, + { + "producer": "academic", + "result": "none_found", + "queries": [ + "working paper bunching £100,000 childcare entitlement threshold UK cost of removing" + ], + "checked": [], + "claims": [], + "note": "CenTax's report (authors at Warwick and LSE) is logged under centax." + }, + { + "producer": "open_search", + "result": "found_not_staged", + "queries": [ + "cost of removing £100,000 childcare cliff edge 30 hours estimate 2026" + ], + "checked": [ + "https://www.itv.com/news/2026-10-03/tories-pledge-to-end-100000-childcare-cliff-edge-on-eve-of-conference", + "https://www.cityam.com/badenoch-vows-to-end-100k-childcare-cliff-edge-introduced-under-last-tory-government/", + "https://www.conservatives.com/news/conservatives-pledge-to-abolish-absurd-childcare-cliff-edge" + ], + "claims": [], + "note": "The Conservative Party put the cost of its pledge at about £700m a year (ITV, 3 October 2026; City AM says the costing is based on CenTax's report). The party's own announcement (conservatives.com, 4 October 2026) gives no figure or method, so there is no primary to stage." + } + ] + } + ] +} diff --git a/tests/test_build_chain.py b/tests/test_build_chain.py index 0be156459..bf449c131 100644 --- a/tests/test_build_chain.py +++ b/tests/test_build_chain.py @@ -58,6 +58,7 @@ "uk_ab2025_verdicts", "uk_ab2025_lanes", "nz_budget_scores", + "analysis_comparables", "be_pit_reform", "be_jrc", ) From 455a1f51feb04fb3ed71b296eca994df27a26597 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 11:28:32 -0400 Subject: [PATCH 02/15] Test the comparables gate, document it, and regenerate the feeds tests/test_comparables.py states and tests the invariants: every committed record passes and the index is what the records build; the gate fails exactly when an unexplained gap exceeds the threshold (Hypothesis); the gap rule's identities; printed values parse back; each rule fails a record that breaks it; the database holds every claim held out with the record's status. The index carries every staged estimate for a How we compare section. docs/COMPARABLES.md is the contract. Analysis claims carry conditions.country so the app files them under their country. Co-Authored-By: Claude Opus 5.5 --- README.md | 8 + app/public/data/lanes.json | 24 +- app/public/data/populations.json | 2211 +++++++++++++++---- app/src/sourceLabels.ts | 11 + data/comparables/index.json | 424 +++- data/populations.json | 2211 +++++++++++++++---- docs/ARCHITECTURE.md | 9 + docs/COMPARABLES.md | 154 ++ scorecard_db/comparables.py | 38 + scorecard_db/ingest_analysis_comparables.py | 2 + tests/test_comparables.py | 852 +++++++ tests/test_export_populations.py | 8 +- 12 files changed, 5000 insertions(+), 952 deletions(-) create mode 100644 docs/COMPARABLES.md create mode 100644 tests/test_comparables.py diff --git a/README.md b/README.md index 489dc3b01..08ec3aa3d 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,14 @@ python pipeline/compute_tariff_counterparts.py # parquet if present, else the python pipeline/build_moments.py # -> app/public/data/moments.json ``` +## Pre-publish comparables + +Every PolicyEngine analysis is searched against every external producer +before it is published, and each estimate found is recorded here with the +cause of any gap over 25% (docs/COMPARABLES.md). One directory per +analysis, `sources/analysis--/`; the validator and index are +`scorecard_db/comparables.py`, and policyengine-app-v2's CI reads the index. + ## Reproducing Python side (requires the policyengine.py-managed environment; heavy — diff --git a/app/public/data/lanes.json b/app/public/data/lanes.json index 0857fa242..efb8da0ae 100644 --- a/app/public/data/lanes.json +++ b/app/public/data/lanes.json @@ -4,7 +4,7 @@ "country": "US | UK | BE | NZ \u2014 the model instance the lane feeds (issue #42)", "note": "Repo-committed status feed (issue #7); graduate to Supabase when lane count makes commits noisy. Every lane writes its transition here." }, - "updated": "2026-10-19", + "updated": "2026-10-21", "lanes": [ { "id": "urban-sotsn", @@ -577,6 +577,28 @@ "running": false, "updated": "2026-10-15", "note": "88 of 132 claims computed \u2014 State individual income tax collections (T40) for 41 States, DC, NH and WA, FY2023-FY2025; FY2023 (a us-6.2.1 calibration target) and FY2025 compared with PE's CY2024 State income tax on us-6.2.1; adapter QC checks the US column against the 50 States on all 25 items; admin outturns staged as claims by decision on #176" + }, + { + "id": "analysis-comparables-uk", + "source": "Pre-publish comparables", + "area": "every external estimate of each PolicyEngine analysis, found before it is published", + "mode": 2, + "country": "UK", + "stage": "computed", + "running": false, + "updated": "2026-10-21", + "note": "1 analyses searched, 20 claims, 6 compared with the analysis's own estimate" + }, + { + "id": "analysis-comparables-us", + "source": "Pre-publish comparables", + "area": "every external estimate of each PolicyEngine analysis, found before it is published", + "mode": 2, + "country": "US", + "stage": "registered", + "running": false, + "updated": "2026-10-21", + "note": "0 analyses searched, 0 claims, 0 compared with the analysis's own estimate" } ] } diff --git a/app/public/data/populations.json b/app/public/data/populations.json index 0a664f1fd..bf1996a00 100644 --- a/app/public/data/populations.json +++ b/app/public/data/populations.json @@ -1,8 +1,8 @@ { - "built": "2026-10-19", + "built": "2026-10-21", "note": "Non-Urban populations exported from scorecard.db: the populace reform-validation registry (issue #20) plus the compute campaign's attached comparisons (US: TPC/CPSP/PWBM/CBO/JCT; UK: the HMRC ready-reckoner family and the complete OBR Policy measures database, including historical HM Treasury scores; BE: JRC EUROMOD-BE claims with demo-grade Axiom concept-mismatch attachments and the 2026 PIT-reform scorekeeper claims; NZ: official budget scores). Claims explicitly opted into pre-result display remain in the feed with latest status not_computed and empty history. Recorded result statuses and calibration relationships are verbatim; nothing here is a pass/fail grade.", "summary": { - "claims": 29717, + "claims": 29737, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -16,7 +16,8 @@ "census": 104, "census_pep": 1, "census_stc": 88, - "centax": 80, + "centax": 92, + "centre_for_british_progress": 8, "co_admin": 3, "co_fiscal_note": 1, "cour_des_comptes": 1, @@ -108,14 +109,14 @@ "by_latest_status": { "comparable": 42, "concept_mismatch": 17, - "constructed": 1662, - "not_computed": 27107, + "constructed": 1668, + "not_computed": 27121, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28945, + "same": 28965, "unknown": 269 }, "by_country": { @@ -134,10 +135,10 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29161, - "with_result": 2162, - "with_value": 1177, - "without_result": 26999, + "claims": 29181, + "with_result": 2168, + "with_value": 1183, + "without_result": 27013, "multi_release_claims": 0 }, "US": { @@ -24888,43 +24889,43 @@ } }, { - "claim_id": "2a18eefc3c44336f3a17", + "claim_id": "4a0f85b93849b6dd88c2", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from the reform", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "farm estates with an APR/BPR claim covering less than 20% of the estate (likely passive investors)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "2%" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-520m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.02, + "reform_key": "db11caf3e07ba4d1", + "external_value": -520000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.02, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -520000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -24946,102 +24947,118 @@ "diagnosis": null }, { - "claim_id": "392f8284d4c31f166150", + "claim_id": "c7b61b17226c7541f264", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "range_low", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "estimate_kind": "range_low", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted estates worth over \u00a35 million", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Over 80%" + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-800m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.8, + "reform_key": "db11caf3e07ba4d1", + "external_value": -800000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.8, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -800000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.5425, + "delta": 366000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], "diagnosis": null }, { - "claim_id": "74553c98c56cca6455f2", + "claim_id": "4b5f3820c010baa7f581", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted estates over \u00a310 million", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "55%" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-610m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.55, + "reform_key": "db11caf3e07ba4d1", + "external_value": -610000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.55, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -610000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25063,101 +25080,118 @@ "diagnosis": null }, { - "claim_id": "878f5fd8884fa99dd180", + "claim_id": "529d98a827c2481cd335", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_high_cost_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from the reform", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "\u2018small family farms\u2019 (APR/BPR share > 60% of estate and estate value < \u00a35m)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "15%" + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-820m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.15, + "reform_key": "db11caf3e07ba4d1", + "external_value": -820000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.15, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -820000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.5292682926829269, + "delta": 386000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], "diagnosis": null }, { - "claim_id": "8d00404bfd0eecdebbdd", + "claim_id": "abb782333de07295c157", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_low_cost_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with income over \u00a3100,000", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Almost half (47%)" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "+140m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.47, + "reform_key": "db11caf3e07ba4d1", + "external_value": 140000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.47, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 140000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25179,130 +25213,118 @@ "diagnosis": null }, { - "claim_id": "adfa5c7278bede4c71c6", + "claim_id": "14a2b2f5e58c26103463", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_low_cost_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "decile_10", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "98%" - }, + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-580m" + }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.98, + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.98, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.7482758620689656, + "delta": 146000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "b546dfcd9e2a9adfafc9", + "claim_id": "db96c1be69d70c9783dd", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2029", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with income over \u00a3500,000", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "23%" + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-640m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.23, + "reform_key": "db11caf3e07ba4d1", + "external_value": -640000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.23, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -640000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25324,131 +25346,118 @@ "diagnosis": null }, { - "claim_id": "b9572e40f5a923d1494a", + "claim_id": "145a9fa05c8bfec614be", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Central Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "range_high", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2029", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "estimate_kind": "range_high", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "bottom_50pct", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "less than 0.4%" + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-980m" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.004, + "reform_key": "db11caf3e07ba4d1", + "external_value": -980000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.004, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -980000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.5153061224489796, + "delta": 475000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "b97a79c5409ab13c7372", + "claim_id": "64ea2993d719166f5714", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2029", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with total income under \u00a325,000 per year (five-year average before death)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Around 10%" + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-770m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.1, + "reform_key": "db11caf3e07ba4d1", + "external_value": -770000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.1, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -770000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25470,100 +25479,223 @@ "diagnosis": null }, { - "claim_id": "ccd04b8d9b2351d508ba", + "claim_id": "1ab38121c6bdb17bcb7c", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "High-Cost Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "London", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_high_cost_fy2029", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "London", - "geography_level": "ITL1", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "46%" + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-1,010m" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.46, + "reform_key": "db11caf3e07ba4d1", + "external_value": -1010000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.46, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -1010000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.5, + "delta": 505000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "d207cb71ef81a7f6d14e", + "claim_id": "f460a20fb80ee6108e62", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_low_cost_fy2029", + "basis": "net of intensive and extensive behavioural margins", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "+160m" + }, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": 160000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 160000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "2ad7d14489e0a073fb55", + "source": "centax", + "country": "UK", + "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_low_cost_fy2029", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-700m" + }, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -700000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -700000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.7214285714285714, + "delta": 195000000.0 + }, + "results": [ + { + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], + "diagnosis": null + }, + { + "claim_id": "2a18eefc3c44336f3a17", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", "unit_concept": "share", "value_kind": "point", "period": 2027, @@ -25573,76 +25705,47 @@ "geography": "UK", "program": null, "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", "basis": "static", "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from the reform", "fiscal_event": "autumn_budget_2025", "fy": "2026-27", "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "top_0.1pct", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "58%" + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "farm estates with an APR/BPR claim covering less than 20% of the estate (likely passive investors)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "2%" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.58, + "reform_key": "78c68f7e47327f86", + "external_value": 0.02, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.58, + "external_value_pe_sign": 0.02, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", - "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" - ], - "baseline": "current_law", - "status_effective": "pe_gap", + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", "ratio": null, "delta": null }, - "results": [ - { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", - "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", - "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" - ], - "baseline": "current_law", - "status_effective": "pe_gap" - } - ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "results": [], + "diagnosis": null }, { - "claim_id": "dd2cde755a07c0ed9ba7", + "claim_id": "392f8284d4c31f166150", "source": "centax", "country": "UK", "source_column": "", @@ -25652,7 +25755,7 @@ "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", "metric": "revenue_share", "unit_concept": "share", - "value_kind": "range_high", + "value_kind": "range_low", "period": 2027, "time_basis": "fiscal_year", "period_start": null, @@ -25664,22 +25767,22 @@ "benchmark_class": "different_model", "country": "UK", "denominator": "additional tax from farm estates", - "estimate_kind": "range_high", + "estimate_kind": "range_low", "fiscal_event": "autumn_budget_2025", "fy": "2026-27", "geography": "UK", "measure_type": "per year of deaths, uprated to 2027", "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted farm estates valued at less than \u00a32 million", + "subgroup": "impacted estates worth over \u00a35 million", "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Less than 1%" + "value_verbatim": "Over 80%" }, "reform_framework": "policy_ref", "reform_key": "78c68f7e47327f86", - "external_value": 0.01, + "external_value": 0.8, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.01, + "external_value_pe_sign": 0.8, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25701,39 +25804,43 @@ "diagnosis": null }, { - "claim_id": "3de3f320ce2d052aec1c", + "claim_id": "74553c98c56cca6455f2", "source": "centax", "country": "UK", "source_column": "", - "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", "window": "", - "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", - "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", - "metric": "taxpayer_count", - "unit_concept": "persons", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", "value_kind": "point", - "period": 2020, + "period": 2027, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "Kensington", + "geography": "UK", "program": null, "conditions": { - "benchmark_class": "administrative_fact", + "basis": "static", + "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from farm estates", "fiscal_event": "autumn_budget_2025", - "fy": "2019-20", - "geography": "Kensington", - "geography_level": "westminster_constituency", - "subgroup": "partners", - "value_verbatim": "1800 partners" + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "impacted estates over \u00a310 million", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "55%" }, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 1800.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.55, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 1800.0, + "external_value_pe_sign": 0.55, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25755,35 +25862,727 @@ "diagnosis": null }, { - "claim_id": "55e1fd35afbb78cc5b82", + "claim_id": "878f5fd8884fa99dd180", "source": "centax", "country": "UK", "source_column": "", - "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", "window": "", - "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", - "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", - "metric": "taxpayer_count", - "unit_concept": "persons", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", "value_kind": "point", - "period": 2020, + "period": 2027, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "Wales and Northern Ireland", + "geography": "UK", "program": null, "conditions": { - "benchmark_class": "administrative_fact", + "basis": "static", + "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from the reform", "fiscal_event": "autumn_budget_2025", - "fy": "2019-20", - "geography": "Wales and Northern Ireland", - "subgroup": "partners", - "value_verbatim": "65,000 partners" + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "\u2018small family farms\u2019 (APR/BPR share > 60% of estate and estate value < \u00a35m)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "15%" }, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 65000.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.15, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.15, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "8d00404bfd0eecdebbdd", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with income over \u00a3100,000", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Almost half (47%)" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.47, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.47, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "adfa5c7278bede4c71c6", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "decile_10", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "98%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.98, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.98, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "b546dfcd9e2a9adfafc9", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with income over \u00a3500,000", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "23%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.23, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.23, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "b9572e40f5a923d1494a", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "range_high", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "range_high", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "bottom_50pct", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "less than 0.4%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.004, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.004, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "b97a79c5409ab13c7372", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with total income under \u00a325,000 per year (five-year average before death)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Around 10%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.1, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.1, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "ccd04b8d9b2351d508ba", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "London", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "London", + "geography_level": "ITL1", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "46%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.46, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.46, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "d207cb71ef81a7f6d14e", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "top_0.1pct", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "58%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.58, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.58, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "dd2cde755a07c0ed9ba7", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "range_high", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "estimate_kind": "range_high", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "impacted farm estates valued at less than \u00a32 million", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Less than 1%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.01, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.01, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "3de3f320ce2d052aec1c", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "window": "", + "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", + "metric": "taxpayer_count", + "unit_concept": "persons", + "value_kind": "point", + "period": 2020, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "Kensington", + "program": null, + "conditions": { + "benchmark_class": "administrative_fact", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2019-20", + "geography": "Kensington", + "geography_level": "westminster_constituency", + "subgroup": "partners", + "value_verbatim": "1800 partners" + }, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 1800.0, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 1800.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "55e1fd35afbb78cc5b82", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "window": "", + "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", + "metric": "taxpayer_count", + "unit_concept": "persons", + "value_kind": "point", + "period": 2020, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "Wales and Northern Ireland", + "program": null, + "conditions": { + "benchmark_class": "administrative_fact", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2019-20", + "geography": "Wales and Northern Ireland", + "subgroup": "partners", + "value_verbatim": "65,000 partners" + }, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 65000.0, "sign_relative_to_pe": "same", "sign_basis": "level", "external_value_pe_sign": 65000.0, @@ -25807,6 +26606,470 @@ "results": [], "diagnosis": null }, + { + "claim_id": "eef80e8be4769cc2ce16", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2026/27", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.330" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 330000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 330000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "f40fe6519a80e65479f2", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2026/27", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.070" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 70000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 70000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "123f8c0626dfc2a46e5d", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2027/28", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.349" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 349000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 349000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "ba4937e8a058012d9f58", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2027/28", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.077" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 77000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 77000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "985648c9838b5184dc10", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2028/29", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.399" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 399000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 399000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "d6f07da73635a2c1e185", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2028/29", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.091" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 91000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 91000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "3a5f40190cc9637435d5", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2029/30", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2029", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.427" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 427000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 427000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "68b30471cf9f5a87368c", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2029/30", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2029", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.132" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 132000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 132000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, { "claim_id": "03d666ddda6fe83bd6f4", "source": "co_admin", diff --git a/app/src/sourceLabels.ts b/app/src/sourceLabels.ts index 4d64dd7a5..253cc57c1 100644 --- a/app/src/sourceLabels.ts +++ b/app/src/sourceLabels.ts @@ -61,6 +61,17 @@ const SOURCE_SPECIAL: Record = { loughborough_crsp: "Loughborough CRSP", trussell_wpi: "Trussell / WPI Economics", niesr: "NIESR", + // Pre-publish comparables (docs/COMPARABLES.md): producers on + // data/comparables/producers.json that no earlier lane staged. (The + // Centre for British Progress label arrives with #207.) + dfe: "DfE", + lse_case: "LSE CASE", + landman_economics: "Landman Economics", + academic: "Academic working paper", + urban: "Urban Institute", + cbpp: "CBPP", + itep: "ITEP", + crfb: "CRFB", }; export function sourceLabel(s: string): string { diff --git a/data/comparables/index.json b/data/comparables/index.json index 5014de718..fc0da681e 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -59,7 +59,429 @@ "unbounded_gaps": 0, "max_abs_gap": 0.5, "tracking_issues": [], - "search_log": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/sources/analysis-uk-childcare-100k-threshold/search_log.json" + "search_log": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/sources/analysis-uk-childcare-100k-threshold/search_log.json", + "comparisons": [ + { + "claim": "centax_static_central_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": -800000000, + "value_raw": "-800m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2027", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", + "pe_value": -434000000, + "gap": -0.4575, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_central_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": -520000000, + "value_raw": "-520m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "centax_static_central_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": -980000000, + "value_raw": "-980m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2029", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", + "pe_value": -505000000, + "gap": -0.4847, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_central_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": -640000000, + "value_raw": "-640m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "centax_static_high_cost_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": -820000000, + "value_raw": "-820m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2027", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", + "pe_value": -434000000, + "gap": -0.4707, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_high_cost_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": -610000000, + "value_raw": "-610m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "centax_static_high_cost_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": -1010000000, + "value_raw": "-1,010m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2029", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", + "pe_value": -505000000, + "gap": -0.5, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_high_cost_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": -770000000, + "value_raw": "-770m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "centax_static_low_cost_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": -580000000, + "value_raw": "-580m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2027", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", + "pe_value": -434000000, + "gap": -0.2517, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_low_cost_fy2027", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": 140000000, + "value_raw": "+140m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "centax_static_low_cost_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": -700000000, + "value_raw": "-700m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2029", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", + "pe_value": -505000000, + "gap": -0.2786, + "status": "constructed", + "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "pe_missing": null + }, + { + "claim": "centax_net_low_cost_fy2029", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table 4.2, printed page 41 (PDF page 42)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": 160000000, + "value_raw": "+160m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table2_fy2026", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 2 (PDF page 8)", + "metric": "revenue_change", + "period": 2027, + "fy": "2026-27", + "value": 70000000, + "value_raw": "0.070", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table2_fy2027", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 2 (PDF page 8)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": 77000000, + "value_raw": "0.077", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table2_fy2028", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 2 (PDF page 8)", + "metric": "revenue_change", + "period": 2029, + "fy": "2028-29", + "value": 91000000, + "value_raw": "0.091", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table2_fy2029", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 2 (PDF page 8)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": 132000000, + "value_raw": "0.132", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table3_fy2026", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 3 (PDF page 10)", + "metric": "revenue_change", + "period": 2027, + "fy": "2026-27", + "value": 330000000, + "value_raw": "0.330", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table3_fy2027", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 3 (PDF page 10)", + "metric": "revenue_change", + "period": 2028, + "fy": "2027-28", + "value": 349000000, + "value_raw": "0.349", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table3_fy2028", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 3 (PDF page 10)", + "metric": "revenue_change", + "period": 2029, + "fy": "2028-29", + "value": 399000000, + "value_raw": "0.399", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + }, + { + "claim": "cbp_net_table3_fy2029", + "source": "centre_for_british_progress", + "publisher": "Centre for British Progress", + "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "locator": "section 1.3.7, Table 3 (PDF page 10)", + "metric": "revenue_change", + "period": 2030, + "fy": "2029-30", + "value": 427000000, + "value_raw": "0.427", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "status": null, + "cause": null, + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + } + ] } } } diff --git a/data/populations.json b/data/populations.json index 0a664f1fd..bf1996a00 100644 --- a/data/populations.json +++ b/data/populations.json @@ -1,8 +1,8 @@ { - "built": "2026-10-19", + "built": "2026-10-21", "note": "Non-Urban populations exported from scorecard.db: the populace reform-validation registry (issue #20) plus the compute campaign's attached comparisons (US: TPC/CPSP/PWBM/CBO/JCT; UK: the HMRC ready-reckoner family and the complete OBR Policy measures database, including historical HM Treasury scores; BE: JRC EUROMOD-BE claims with demo-grade Axiom concept-mismatch attachments and the 2026 PIT-reform scorekeeper claims; NZ: official budget scores). Claims explicitly opted into pre-result display remain in the feed with latest status not_computed and empty history. Recorded result statuses and calibration relationships are verbatim; nothing here is a pass/fail grade.", "summary": { - "claims": 29717, + "claims": 29737, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -16,7 +16,8 @@ "census": 104, "census_pep": 1, "census_stc": 88, - "centax": 80, + "centax": 92, + "centre_for_british_progress": 8, "co_admin": 3, "co_fiscal_note": 1, "cour_des_comptes": 1, @@ -108,14 +109,14 @@ "by_latest_status": { "comparable": 42, "concept_mismatch": 17, - "constructed": 1662, - "not_computed": 27107, + "constructed": 1668, + "not_computed": 27121, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28945, + "same": 28965, "unknown": 269 }, "by_country": { @@ -134,10 +135,10 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29161, - "with_result": 2162, - "with_value": 1177, - "without_result": 26999, + "claims": 29181, + "with_result": 2168, + "with_value": 1183, + "without_result": 27013, "multi_release_claims": 0 }, "US": { @@ -24888,43 +24889,43 @@ } }, { - "claim_id": "2a18eefc3c44336f3a17", + "claim_id": "4a0f85b93849b6dd88c2", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from the reform", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "farm estates with an APR/BPR claim covering less than 20% of the estate (likely passive investors)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "2%" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-520m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.02, + "reform_key": "db11caf3e07ba4d1", + "external_value": -520000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.02, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -520000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -24946,102 +24947,118 @@ "diagnosis": null }, { - "claim_id": "392f8284d4c31f166150", + "claim_id": "c7b61b17226c7541f264", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "range_low", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "estimate_kind": "range_low", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted estates worth over \u00a35 million", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Over 80%" + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-800m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.8, + "reform_key": "db11caf3e07ba4d1", + "external_value": -800000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.8, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -800000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.5425, + "delta": 366000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], "diagnosis": null }, { - "claim_id": "74553c98c56cca6455f2", + "claim_id": "4b5f3820c010baa7f581", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted estates over \u00a310 million", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "55%" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-610m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.55, + "reform_key": "db11caf3e07ba4d1", + "external_value": -610000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.55, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -610000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25063,101 +25080,118 @@ "diagnosis": null }, { - "claim_id": "878f5fd8884fa99dd180", + "claim_id": "529d98a827c2481cd335", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_high_cost_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from the reform", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "\u2018small family farms\u2019 (APR/BPR share > 60% of estate and estate value < \u00a35m)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "15%" + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-820m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.15, + "reform_key": "db11caf3e07ba4d1", + "external_value": -820000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.15, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -820000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.5292682926829269, + "delta": 386000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], "diagnosis": null }, { - "claim_id": "8d00404bfd0eecdebbdd", + "claim_id": "abb782333de07295c157", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_low_cost_fy2027", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with income over \u00a3100,000", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Almost half (47%)" + "fy": "2027-28", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "+140m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.47, + "reform_key": "db11caf3e07ba4d1", + "external_value": 140000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.47, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 140000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25179,130 +25213,118 @@ "diagnosis": null }, { - "claim_id": "adfa5c7278bede4c71c6", + "claim_id": "14a2b2f5e58c26103463", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_low_cost_fy2027", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "decile_10", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "98%" - }, + "fy": "2027-28", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-580m" + }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.98, + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.98, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.7482758620689656, + "delta": 146000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -434000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "b546dfcd9e2a9adfafc9", + "claim_id": "db96c1be69d70c9783dd", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2029", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with income over \u00a3500,000", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "23%" + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-640m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.23, + "reform_key": "db11caf3e07ba4d1", + "external_value": -640000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.23, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -640000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25324,131 +25346,118 @@ "diagnosis": null }, { - "claim_id": "b9572e40f5a923d1494a", + "claim_id": "145a9fa05c8bfec614be", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Central Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "range_high", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2029", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "estimate_kind": "range_high", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "bottom_50pct", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "less than 0.4%" + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-980m" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.004, + "reform_key": "db11caf3e07ba4d1", + "external_value": -980000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.004, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -980000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.5153061224489796, + "delta": 475000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "b97a79c5409ab13c7372", + "claim_id": "64ea2993d719166f5714", "source": "centax", "country": "UK", - "source_column": "", - "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", - "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "UK", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2029", + "basis": "net of intensive and extensive behavioural margins", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "UK", - "measure_type": "per year of deaths, uprated to 2027", - "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "deceased with total income under \u00a325,000 per year (five-year average before death)", - "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Around 10%" + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-770m" }, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.1, + "reform_key": "db11caf3e07ba4d1", + "external_value": -770000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.1, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -770000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25470,100 +25479,223 @@ "diagnosis": null }, { - "claim_id": "ccd04b8d9b2351d508ba", + "claim_id": "1ab38121c6bdb17bcb7c", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "High-Cost Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", - "unit_concept": "share", - "value_kind": "point", - "period": 2027, + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "London", - "program": null, + "geography": "England", + "program": "free_childcare_hours", "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_high_cost_fy2029", "basis": "static", "benchmark_class": "different_model", "country": "UK", - "fiscal_event": "autumn_budget_2025", - "fy": "2026-27", - "geography": "London", - "geography_level": "ITL1", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "46%" + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "high_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-1,010m" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.46, + "reform_key": "db11caf3e07ba4d1", + "external_value": -1010000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.46, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -1010000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.5, + "delta": 505000000.0 }, "results": [ { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", + "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." ], "baseline": "current_law", - "status_effective": "pe_gap" + "status_effective": "constructed" } ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "diagnosis": null }, { - "claim_id": "d207cb71ef81a7f6d14e", + "claim_id": "f460a20fb80ee6108e62", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", "window": "", - "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", - "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", - "metric": "revenue_share", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_low_cost_fy2029", + "basis": "net of intensive and extensive behavioural margins", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "+160m" + }, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": 160000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 160000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "2ad7d14489e0a073fb55", + "source": "centax", + "country": "UK", + "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_low_cost_fy2029", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "low_cost", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-700m" + }, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -700000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -700000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.7214285714285714, + "delta": 195000000.0 + }, + "results": [ + { + "value": -505000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], + "diagnosis": null + }, + { + "claim_id": "2a18eefc3c44336f3a17", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", "unit_concept": "share", "value_kind": "point", "period": 2027, @@ -25573,76 +25705,47 @@ "geography": "UK", "program": null, "conditions": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", "basis": "static", "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from the reform", "fiscal_event": "autumn_budget_2025", "fy": "2026-27", "geography": "UK", - "incidence": "full incidence on partners, zero behavioural response", - "income_concept": "total income (all taxpayers)", - "income_group": "top_0.1pct", - "measure_key": "ab2025_option__nics_on_partnership_income", - "pe_expressibility": "not_expressible", - "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", - "value_verbatim": "58%" + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "farm estates with an APR/BPR claim covering less than 20% of the estate (likely passive investors)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "2%" }, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.58, + "reform_key": "78c68f7e47327f86", + "external_value": 0.02, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.58, + "external_value_pe_sign": 0.02, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", - "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" - ], - "baseline": "current_law", - "status_effective": "pe_gap", + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", "ratio": null, "delta": null }, - "results": [ - { - "value": null, - "status": "pe_gap", - "engine_version": "2.89.2", - "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", - "release": "8c73-4aa4b14", - "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", - "policyengine_variables": [], - "computed_at": "2026-09-24T00:00:00+00:00", - "annotations": [ - "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", - "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" - ], - "baseline": "current_law", - "status_effective": "pe_gap" - } - ], - "diagnosis": { - "class": "pe_gap", - "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" - } + "results": [], + "diagnosis": null }, { - "claim_id": "dd2cde755a07c0ed9ba7", + "claim_id": "392f8284d4c31f166150", "source": "centax", "country": "UK", "source_column": "", @@ -25652,7 +25755,7 @@ "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", "metric": "revenue_share", "unit_concept": "share", - "value_kind": "range_high", + "value_kind": "range_low", "period": 2027, "time_basis": "fiscal_year", "period_start": null, @@ -25664,22 +25767,22 @@ "benchmark_class": "different_model", "country": "UK", "denominator": "additional tax from farm estates", - "estimate_kind": "range_high", + "estimate_kind": "range_low", "fiscal_event": "autumn_budget_2025", "fy": "2026-27", "geography": "UK", "measure_type": "per year of deaths, uprated to 2027", "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", - "subgroup": "impacted farm estates valued at less than \u00a32 million", + "subgroup": "impacted estates worth over \u00a35 million", "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", - "value_verbatim": "Less than 1%" + "value_verbatim": "Over 80%" }, "reform_framework": "policy_ref", "reform_key": "78c68f7e47327f86", - "external_value": 0.01, + "external_value": 0.8, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.01, + "external_value_pe_sign": 0.8, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25701,39 +25804,43 @@ "diagnosis": null }, { - "claim_id": "3de3f320ce2d052aec1c", + "claim_id": "74553c98c56cca6455f2", "source": "centax", "country": "UK", "source_column": "", - "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", "window": "", - "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", - "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", - "metric": "taxpayer_count", - "unit_concept": "persons", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", "value_kind": "point", - "period": 2020, + "period": 2027, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "Kensington", + "geography": "UK", "program": null, "conditions": { - "benchmark_class": "administrative_fact", + "basis": "static", + "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from farm estates", "fiscal_event": "autumn_budget_2025", - "fy": "2019-20", - "geography": "Kensington", - "geography_level": "westminster_constituency", - "subgroup": "partners", - "value_verbatim": "1800 partners" + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "impacted estates over \u00a310 million", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "55%" }, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 1800.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.55, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 1800.0, + "external_value_pe_sign": 0.55, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -25755,35 +25862,727 @@ "diagnosis": null }, { - "claim_id": "55e1fd35afbb78cc5b82", + "claim_id": "878f5fd8884fa99dd180", "source": "centax", "country": "UK", "source_column": "", - "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", "window": "", - "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", - "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", - "metric": "taxpayer_count", - "unit_concept": "persons", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", "value_kind": "point", - "period": 2020, + "period": 2027, "time_basis": "fiscal_year", "period_start": null, "period_end": null, - "geography": "Wales and Northern Ireland", + "geography": "UK", "program": null, "conditions": { - "benchmark_class": "administrative_fact", + "basis": "static", + "benchmark_class": "different_model", "country": "UK", + "denominator": "additional tax from the reform", "fiscal_event": "autumn_budget_2025", - "fy": "2019-20", - "geography": "Wales and Northern Ireland", - "subgroup": "partners", - "value_verbatim": "65,000 partners" + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "\u2018small family farms\u2019 (APR/BPR share > 60% of estate and estate value < \u00a35m)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "15%" }, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 65000.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.15, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.15, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "8d00404bfd0eecdebbdd", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with income over \u00a3100,000", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Almost half (47%)" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.47, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.47, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "adfa5c7278bede4c71c6", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "decile_10", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "98%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.98, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.98, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "b546dfcd9e2a9adfafc9", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with income over \u00a3500,000", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "23%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.23, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.23, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "b9572e40f5a923d1494a", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "range_high", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "range_high", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "bottom_50pct", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "less than 0.4%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.004, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.004, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "b97a79c5409ab13c7372", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "deceased with total income under \u00a325,000 per year (five-year average before death)", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Around 10%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.1, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.1, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "ccd04b8d9b2351d508ba", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "London", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "London", + "geography_level": "ITL1", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "46%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.46, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.46, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "d207cb71ef81a7f6d14e", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "window": "", + "publication_title": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "url": "https://centax.org.uk/wp-content/uploads/2025/09/AdvaniGazmuribarkerLonsdaleSummers2025_PartnershipNICs.pdf", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "point", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "incidence": "full incidence on partners, zero behavioural response", + "income_concept": "total income (all taxpayers)", + "income_group": "top_0.1pct", + "measure_key": "ab2025_option__nics_on_partnership_income", + "pe_expressibility": "not_expressible", + "pe_missing": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "reform_hint": "\u2018Partnership NICs\u2019: an equivalent to Class 1 Secondary (Employer) NICs at 13.04% on partnership profits above a \u00a35,000 Partners\u2019 Exempt Amount, with a \u00a310,500 Partnership Allowance applied in proportion to each partner's profit share", + "value_verbatim": "58%" + }, + "reform_framework": "policy_ref", + "reform_key": "7718692432a4c4f2", + "external_value": 0.58, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.58, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap", + "ratio": null, + "delta": null + }, + "results": [ + { + "value": null, + "status": "pe_gap", + "engine_version": "2.89.2", + "data_bundle": "populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z", + "release": "8c73-4aa4b14", + "construction": "pe_gap:not_expressible:ab2025_option__nics_on_partnership_income", + "policyengine_variables": [], + "computed_at": "2026-09-24T00:00:00+00:00", + "annotations": [ + "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "Searched BOTH the parameter tree and the VARIABLE list for /partner|llp|self_employment_income|class_4/. Parameters: 5 node(s) ['gov.hmrc.income_tax.bases.taxable_self_employment_income_deductions', 'gov.hmrc.national_insurance.class_4.rates.additional', 'gov.hmrc.national_insurance.class_4.rates.main', 'gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit', 'gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit']. Variables: 6 ['self_employment_income', 'ni_class_4_maximum', 'ni_class_4', 'ni_class_4_main', 'partners_unused_personal_allowance', 'taxable_self_employment_income']. Both are searched because a parameter-only search misses a variable (#106). Re-run at policyengine-uk 2.89.2.", + "no run executed: the verdict is the registry's, stated against the certified engine 2.89.2" + ], + "baseline": "current_law", + "status_effective": "pe_gap" + } + ], + "diagnosis": { + "class": "pe_gap", + "rationale": "Partnership and LLP profits are not identified within self_employment_income on the bundle, and Class 4 NICs (the nearest lever, ni_class_4 at 6% above the LPL) applies to all self-employed; a charge on partners only cannot be targeted.", + "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136" + } + }, + { + "claim_id": "dd2cde755a07c0ed9ba7", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "window": "", + "publication_title": "Policy Report: The Impact of Changes to Inheritance Tax on Farm Estates (landing page, executive summary)", + "url": "https://centax.org.uk/policy-brief-the-impact-of-changes-to-inheritance-tax-on-farm-estates/", + "metric": "revenue_share", + "unit_concept": "share", + "value_kind": "range_high", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "UK", + "program": null, + "conditions": { + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "denominator": "additional tax from farm estates", + "estimate_kind": "range_high", + "fiscal_event": "autumn_budget_2025", + "fy": "2026-27", + "geography": "UK", + "measure_type": "per year of deaths, uprated to 2027", + "reform_hint": "the planned reform: from April 2026 the first \u00a31 million of combined qualifying agricultural and business property keeps 100% APR/BPR relief and relief above that is reduced to 50% (Autumn Budget 2024); estates uprated to 2027, per year of deaths", + "subgroup": "impacted farm estates valued at less than \u00a32 million", + "unit_population": "farm estates (estates of individuals who died owning farm assets on which relief was claimed)", + "value_verbatim": "Less than 1%" + }, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.01, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 0.01, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "3de3f320ce2d052aec1c", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "window": "", + "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", + "metric": "taxpayer_count", + "unit_concept": "persons", + "value_kind": "point", + "period": 2020, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "Kensington", + "program": null, + "conditions": { + "benchmark_class": "administrative_fact", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2019-20", + "geography": "Kensington", + "geography_level": "westminster_constituency", + "subgroup": "partners", + "value_verbatim": "1800 partners" + }, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 1800.0, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 1800.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "55e1fd35afbb78cc5b82", + "source": "centax", + "country": "UK", + "source_column": "", + "name": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "window": "", + "publication_title": "Policy report: Equalising National Insurance on Partnership Income: Revenue and Distributional Effects (landing page, key findings)", + "url": "https://centax.org.uk/policy-report-equalising-national-insurance-on-partnership-income-revenue-and-distributional-effects/", + "metric": "taxpayer_count", + "unit_concept": "persons", + "value_kind": "point", + "period": 2020, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "Wales and Northern Ireland", + "program": null, + "conditions": { + "benchmark_class": "administrative_fact", + "country": "UK", + "fiscal_event": "autumn_budget_2025", + "fy": "2019-20", + "geography": "Wales and Northern Ireland", + "subgroup": "partners", + "value_verbatim": "65,000 partners" + }, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 65000.0, "sign_relative_to_pe": "same", "sign_basis": "level", "external_value_pe_sign": 65000.0, @@ -25807,6 +26606,470 @@ "results": [], "diagnosis": null }, + { + "claim_id": "eef80e8be4769cc2ce16", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2026/27", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.330" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 330000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 330000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "f40fe6519a80e65479f2", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2026/27", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.070" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 70000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 70000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "123f8c0626dfc2a46e5d", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2027/28", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.349" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 349000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 349000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "ba4937e8a058012d9f58", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2027/28", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.077" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 77000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 77000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "985648c9838b5184dc10", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2028/29", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.399" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 399000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 399000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "d6f07da73635a2c1e185", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2028/29", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.091" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 91000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 91000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "3a5f40190cc9637435d5", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2029/30", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table3_fy2029", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "model": "endogenous_labour_no_pension", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.427" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 427000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 427000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, + { + "claim_id": "68b30471cf9f5a87368c", + "source": "centre_for_british_progress", + "country": "UK", + "source_column": "Abolish Cliff, 2029/30", + "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "window": "", + "publication_title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": null, + "program": "free_childcare_hours_and_tax_free_childcare", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "cbp_net_table2_fy2029", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "net of behavioural responses", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "model": "endogenous_pension_exogenous_labour", + "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", + "value_verbatim": "0.132" + }, + "reform_framework": "policy_ref", + "reform_key": "6afa9ff6f3c2b955", + "external_value": 132000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": 132000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": null, + "status": "not_computed", + "engine_version": "", + "data_bundle": "", + "release": "", + "construction": "", + "policyengine_variables": [], + "computed_at": "", + "annotations": [], + "baseline": null, + "status_effective": "not_computed", + "ratio": null, + "delta": null + }, + "results": [], + "diagnosis": null + }, { "claim_id": "03d666ddda6fe83bd6f4", "source": "co_admin", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8398757ee..ee933b269 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -205,6 +205,15 @@ Operating rules for the agent stage: 10102's groups to its total. Claims flow through `scorecard_db/ingest_cbo.py`; no PE counterparts yet. +- **Pre-publish comparables** (`sources/analysis--/`, + lanes `analysis-comparables-uk` and `-us`; docs/COMPARABLES.md): one + directory per PolicyEngine analysis, holding every external estimate a + search of every listed producer found before the analysis was published, + the analysis's own pinned figures, and the cause of every gap over the + threshold. `scorecard_db/comparables.py` is the gate (an incomplete record + fails CI and the build) and writes `data/comparables/index.json`, which + policyengine-app-v2's CI reads before a research entry can merge. + ### Staged for Chronicle, not claims Administrative outturn tables (caseloads, units, spending) are calibration diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md new file mode 100644 index 000000000..b552b416a --- /dev/null +++ b/docs/COMPARABLES.md @@ -0,0 +1,154 @@ +# Pre-publish comparables + +Before PolicyEngine publishes an analysis, someone searches for every external estimate of what +it estimates, records each one here, and explains any large gap. Max set the rule on +2026-10-21 ("how do we ensure we always do a full review of comparables to add to scorecard +before publishing an analysis - pe-wide"). + +The case that prompted it: the £100,000 childcare dashboard went live on 12 October 2026 with the +30 funded hours costing £0.43bn in 2027-28. CenTax had published £0.80bn in September, from HMRC +administrative data. The dashboard even listed CenTax as a benchmark, but nothing required the +gap to be explained, and the cause, a Microcosm hours draw ([microcosm#1126]), was found only +after Arun Advani pointed it out. Its record is +`sources/analysis-uk-childcare-100k-threshold/`; without the annotation it carries, the +validator below fails it on six rows. + +[microcosm#1126]: https://github.com/PolicyEngine/microcosm/issues/1126 + +## How it is enforced + +1. **The search** runs before publishing, as the `policyengine-comparables-review` skill in + PolicyEngine/policyengine-skills (or a Subfleet research job from its brief). It writes a + directory here and opens a scorecard PR. +2. **This repository's CI** validates every directory (`tests/test_comparables.py`, and the build, + which refuses an incomplete record) and checks that `data/comparables/index.json` is exactly + what the directories build. So everything on main is a complete record. +3. **policyengine-app-v2's CI** checks every research entry dated on or after its cutover: the + entry's `comparables` block must name a directory that the index on this repository's main + lists, with the same search date, producers and count. The scorecard PR therefore lands + before the registry entry can. +4. **After publishing**, a weekly launchd job (`comparables-reharvest`) searches again for every + analysis published in the last `reharvest_window_days` (183). A new comparable becomes a row + here; a new gap over the threshold also opens an issue for the analysis's owner. + +## The directory + +`sources/analysis--/`, where `` is the analysis's slug on policyengine.org +(the apps.json slug, or the post's filename without `.md`). + +| File | What it holds | +|---|---| +| `analysis.json` | The registry entry, scope, search terms, the policy worlds it scores, and every PolicyEngine estimate it publishes, each pinned to its results file by repo, commit, path, field and SHA-256 | +| `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`), who ran it, and one entry per listed producer | +| `manifest.jsonl` | One line per primary publication, with its SHA-256 | +| `claims_staged.jsonl` | Every comparable found, staged under the harvest rules | +| `annotations.json` | The status and cause of anything that is not a plain comparison, each citing a basis | + +Start one with: + +```bash +PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk --kind app --slug +``` + +### The search + +`data/comparables/producers.json` lists the producers for each country. A search logs every +producer whose `added` date is on or before the search date, with one of: + +- `found`: the claim keys it found; +- `none_found`: with the queries run or pages checked; +- `found_not_staged`: a comparable exists but its primary cannot be read (harvest rule 8), so the + note says what it is and the pages it was seen on; +- `not_applicable`: only when the producer's `applies_to` excludes the analysis's scope (the + Scottish Fiscal Commission for a UK-wide analysis, state fiscal notes for a federal one). + +`open_search` and `academic` are on every list: a comparable from a producer not on the list is +logged under them and keeps its producer's own slug. When an open search keeps finding one +producer, add it to the list with a `why` (the first search added DfE this way). + +### The claims + +Staged under the harvest rules (`sources/harvest-uk-ab2025-2026-09-24/README.md`), checked +mechanically where they can be: + +- **Verbatim values.** `value_raw` is the string as printed; `value` is it under a registered + `normalization`; the validator parses one back to the other, and `quote` must contain the + printed value. A claim is never re-signed: its sign convention travels in + `conditions.sign_convention`, in words `scorecard_db/signs.py` reads. +- **Registered vocabulary** for `metric` and `unit_concept`, or `null` with a proposal (such a + claim is staged but waits for its vocabulary decision before it reaches the database). +- **Named baselines**: `baseline_policy` is a label in `scorecard_db/baselines.py`, or null for + current law. +- **Attribution and benchmark class.** Only a producer's own estimate (`attribution: own`) is + compared. An administrative level is calibration material and belongs in a producer-level + source, where `relationships.py` assigns it. +- **Primaries, hashed**: `publication.primary_sha256` is a line in `manifest.jsonl`. + +### The comparison + +A claim either names the PolicyEngine estimate it is compared with (`compare_to`), or says in +`pe_missing` why there is none. A compared claim has the estimate's metric, unit, period and time +basis. When the two score different policy worlds, `compare_to.across_worlds` gives the reason +the quantities match and its basis, and the comparison is `constructed`, not `comparable`. + +The gap is PolicyEngine's value over the external value, minus one, with the external value in +PolicyEngine's orientation (`signs.py`; an unsigned magnitude compares absolute values). A +nonzero estimate against a published zero is over any threshold. Each PolicyEngine estimate is +stated in PolicyEngine's own orientation for its metric; when the results file prints it the +other way round (a cost as a positive number), the estimate carries `sign: -1` and a +`sign_basis`. + +### The threshold + +`data/comparables/policy.json` `gap_threshold`, 0.25 by default. A compared claim whose absolute +gap exceeds it needs an annotation with `explains_gap: true`: its `cause`, and a `basis` that is a +GitHub issue or pull request, engine metadata pinned to a commit, a document, or a measured +diagnostic. This is the scorecard's existing rule against fabricated mechanisms: a cause that +cannot cite one of those does not ship. A miss is allowed; an unexplained one is not. + +A comparable that a post-publication re-harvest finds may instead carry a `pending` annotation +naming the issue opened for the owner; the index counts it until the cause is written. + +### The database + +`scorecard_db/ingest_analysis_comparables.py` (build step `analysis_comparables`) puts every +record on the scorecard: claims held out (another producer's estimate is a comparator, never a +calibration target), with `conditions.analysis` naming the analysis; one result per compared +claim, the analysis's own figure with its record's status; uncompared claims published without a +result. Lanes `analysis-comparables-uk` and `analysis-comparables-us` show the tally. + +## The index and the app-v2 record + +`python -m scorecard_db.comparables index` writes `data/comparables/index.json`, keyed by +directory. policyengine-app-v2 reads it from this repository's main branch (the repository is +public, so a raw fetch needs no token). The block to paste into the analysis's apps.json or +posts.json entry is printed by: + +```bash +PYTHONPATH=. python -m scorecard_db.comparables record analysis-uk- +``` + +```json +"comparables": { + "status": "reviewed", + "scorecardSource": "analysis-uk-", + "searchDate": "2026-10-21", + "searchKind": "prepublish", + "producersSearched": ["academic", "centax", "..."], + "found": 20, + "searchLog": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/sources/analysis-uk-/search_log.json" +} +``` + +An entry that publishes no estimate (an event, an announcement) declares +`{"status": "no_estimate", "reason": "..."}` instead; app-v2 refuses that for an entry tagged +`policy`. + +## Re-harvest support + +```bash +# analyses published in the window, and whether each has a record +PYTHONPATH=. python -m scorecard_db.comparables due --apps apps.json --posts posts.json +# what a re-harvest found since a date, and its gaps over the threshold +PYTHONPATH=. python -m scorecard_db.comparables report analysis-uk- --since 2026-10-26 +``` diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 6d681501e..3225b8d80 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -1032,9 +1032,47 @@ def index_entry(record: AnalysisRecord, policy: dict) -> dict: {r["tracking_issue"] for r in rows if r["tracking_issue"]} ), "search_log": f"{REPO_URL}/blob/main/sources/{record.name}/search_log.json", + "comparisons": _comparisons(record, rows), } +def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: + """Every staged estimate as a reader would see it: the producer's figure + as printed, ours where one compares, the gap and why. The data a "How we + compare" section on the analysis's page reads.""" + claims = {c["key"]: c for c in record.claims} + estimates = {e["key"]: e for e in record.analysis["pe_estimates"]} + causes = {n["id"]: n["cause"] for n in record.annotations} + out = [] + for row in rows: + c = claims[row["key"]] + est = estimates.get(row["estimate"]) if row["estimate"] else None + out.append( + { + "claim": c["key"], + "source": c["source"], + "publisher": c["publication"]["publisher"], + "title": c["publication"]["title"], + "url": c["publication"]["url"], + "locator": c["publication"]["locator"], + "metric": c.get("metric") or c.get("proposed_metric"), + "period": c["period"], + "fy": (c.get("conditions") or {}).get("fy"), + "value": c["value"], + "value_raw": c["value_raw"], + "sign_convention": (c.get("conditions") or {}).get("sign_convention"), + "estimate": row["estimate"], + "estimate_label": est["label"] if est else None, + "pe_value": est["value"] if est else None, + "gap": _round(row["gap"]), + "status": row["status"], + "cause": causes.get(row["annotation"]), + "pe_missing": c.get("pe_missing"), + } + ) + return out + + def build_index( root: Path = SOURCES, producers: dict | None = None, policy: dict | None = None ) -> dict: diff --git a/scorecard_db/ingest_analysis_comparables.py b/scorecard_db/ingest_analysis_comparables.py index cab898c30..7e10cb91b 100644 --- a/scorecard_db/ingest_analysis_comparables.py +++ b/scorecard_db/ingest_analysis_comparables.py @@ -138,6 +138,8 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] reform = _reform(record, c["world"], c.get("baseline_policy")) conditions = { **c["conditions"], + # the app files a row under conditions.country (US when absent) + "country": a["country"], "analysis": a["id"], "analysis_claim": c["key"], "benchmark_class": c["benchmark_class"], diff --git a/tests/test_comparables.py b/tests/test_comparables.py new file mode 100644 index 000000000..4c2247b68 --- /dev/null +++ b/tests/test_comparables.py @@ -0,0 +1,852 @@ +"""The pre-publish comparables gate (docs/COMPARABLES.md). + +Invariants, each tested below: + +- Completeness: every committed sources/analysis-* directory passes the + validator, and the committed index is exactly what the directories build + (twice, identically). +- The gate: a compared claim whose gap exceeds the threshold fails unless an + annotation explains it, for any PolicyEngine and external values; within the + threshold it passes with no annotation (Hypothesis). +- The gap rule: zero for equal values, invariant to rescaling both values, + orientation-consistent (an opposite-signed convention equals the same value + negated), sign-free for magnitudes, monotone in the threshold, and over any + threshold when a nonzero estimate meets a published zero. +- Harvest rule 1: a printed value parses back to the staged value, in every + printed form (Hypothesis), and a re-signed or mis-scaled value fails. +- Coverage: a search that skips any listed producer, or calls an applicable + producer not_applicable, fails; Max's 2026-10-21 producer list is pinned. +- The database: every analysis claim is held out, every compared claim has + exactly one result whose status the record decides, and every uncompared + claim says why. +- Differential: the app-v2 record and the index agree on the search they cite. +""" + +from __future__ import annotations + +import copy +import json +import math +import sqlite3 +from pathlib import Path + +import pytest +from hypothesis import assume, given, settings, strategies as st + +from scorecard_db import comparables as cmp +from scorecard_db import signs + +REPO = Path(__file__).resolve().parent.parent +COMMITTED_DB = REPO / "data" / "scorecard.db" +CHILDCARE = cmp.SOURCES / "analysis-uk-childcare-100k-threshold" +PRODUCERS = cmp.load_producers() +POLICY = cmp.load_policy() +THRESHOLD = POLICY["gap_threshold"] + +# Max's list of 2026-10-21. Additions are fine; removing one of these fails. +MAX_LIST = { + "UK": { + "obr", + "hm_treasury", + "ifs", + "resolution_foundation", + "centax", + "centre_for_british_progress", + "ippr", + "fabian_society", + "niesr", + "ukmod", + "lse_case", + "jrf", + "landman_economics", + "tax_policy_associates", + "academic", + }, + "US": { + "cbo", + "jct", + "tpc", + "pwbm", + "budget_lab", + "tax_foundation", + "urban", + "cbpp", + "itep", + "state_fiscal_note", + "academic", + }, +} + + +# --- a minimal synthetic record ------------------------------------------------ + +SHA = "a" * 64 +COMMIT = "b" * 40 +SEARCH_DATE = "2026-10-21" + + +def synthetic( + pe_value: float, ext_value: float, *, annotate: bool = False +) -> cmp.AnalysisRecord: + """One UK analysis, one estimate, one compared claim; every producer + logged none_found except the one that found the claim.""" + claim_key = "ext_cost_fy2027" + producers = [] + for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE): + entry = { + "producer": p["slug"], + "result": "none_found", + "queries": [f"{p['slug']} test query"], + "checked": [], + "claims": [], + } + if p["slug"] == "ifs": + entry.update(result="found", claims=[claim_key]) + if not cmp.applies(p, "uk_wide"): + entry.update(result="not_applicable", queries=[]) + producers.append(entry) + analysis = { + "schema": cmp.ANALYSIS_SCHEMA, + "id": "uk/test-analysis", + "country": "UK", + "registry": {"kind": "app", "slug": "test-analysis", "country_id": "uk"}, + "title": "A test analysis", + "url": "https://policyengine.org/uk/test-analysis", + "published": None, + "owner": {"author": "max-ghenis"}, + "scope": "uk_wide", + "subject": "The cost of a test reform", + "search_terms": ["test reform cost"], + "worlds": { + "test_reform": { + "policy": "uk_test_reform", + "description": "A test reform world", + } + }, + "pe_estimates": [ + { + "key": "pe_cost_fy2027", + "label": "Cost, 2027-28", + "world": "test_reform", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": pe_value, + "value_raw": repr(abs(pe_value)) if pe_value else "0", + "normalization": "as printed", + "sign": -1 if pe_value < 0 else 1, + "sign_basis": "printed as a positive cost; revenue_change is positive = yield", + "conditions": {"sign_convention": "positive = yield to the Exchequer"}, + "provenance": { + "repo": "PolicyEngine/test", + "commit": COMMIT, + "path": "results.json", + "sha256": SHA, + "field": "cost", + "computed_at": "2026-10-20", + "engine": {"policyengine": "6.2.1"}, + "dataset": "test-dataset", + }, + } + ], + } + raw = repr(abs(ext_value)) + claim = { + "key": claim_key, + "source": "ifs", + "found_by": "ifs", + "found_on": SEARCH_DATE, + "source_model": "ifs_taxben", + "metric": "revenue_change", + "unit_concept": "gbp", + "value": ext_value, + "value_raw": ("-" if ext_value < 0 else "") + raw, + "normalization": "as printed", + "value_kind": "point", + "period": 2028, + "time_basis": "fiscal_year", + "conditions": {"sign_convention": "positive = yield to the Exchequer"}, + "world": "test_reform", + "reform_hint": "the test reform", + "attribution": "own", + "benchmark_class": "different_model", + "baseline_policy": None, + "publication": { + "title": "A test report", + "url": "https://example.org/report.pdf", + "date": "2026-10-01", + "publisher": "IFS", + "primary_sha256": SHA, + "locator": "Table 1", + "access": "direct", + }, + "source_table": "Table 1", + "source_column": "Cost", + "quote": f"Cost {('-' if ext_value < 0 else '') + raw}", + "parse_confidence": "high", + "status": "ok", + "compare_to": {"pe_estimate": "pe_cost_fy2027"}, + } + annotations = [] + if annotate: + annotations.append( + { + "id": "test-cause", + "claims": [claim_key], + "status": "comparable", + "explains_gap": True, + "cause": "A documented difference in the test populations explains it.", + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/1", + } + ], + } + ) + return cmp.AnalysisRecord( + directory=Path("/nonexistent/analysis-uk-test-analysis"), + analysis=analysis, + search_log={ + "schema": cmp.SEARCH_SCHEMA, + "searches": [ + { + "date": SEARCH_DATE, + "kind": "prepublish", + "by": "test", + "producers": producers, + } + ], + }, + manifest=[ + { + "url": "https://example.org/report.pdf", + "title": "A test report", + "date": "2026-10-01", + "publisher": "IFS", + "sha256": SHA, + "retrieved": SEARCH_DATE, + "access": "direct", + } + ], + claims=[claim], + annotations=annotations, + ) + + +def errors_of(record) -> list[str]: + return cmp.validate(record, PRODUCERS, POLICY) + + +def childcare() -> cmp.AnalysisRecord: + return cmp.load_analysis(CHILDCARE) + + +# --- completeness of what is committed --------------------------------------------- + + +@pytest.mark.parametrize("directory", cmp.analysis_dirs(), ids=lambda d: d.name) +def test_every_committed_record_passes_the_gate(directory): + assert errors_of(cmp.load_analysis(directory)) == [] + + +def test_committed_index_is_what_the_records_build(): + assert cmp.INDEX_PATH.read_text() == cmp.render_index(cmp.build_index()) + + +def test_index_build_is_deterministic(): + assert cmp.render_index(cmp.build_index()) == cmp.render_index(cmp.build_index()) + + +def test_index_lists_every_record_and_only_records(): + index = json.loads(cmp.INDEX_PATH.read_text()) + assert sorted(index["analyses"]) == [d.name for d in cmp.analysis_dirs()] + assert index["gap_threshold"] == THRESHOLD + + +def test_max_producer_list_is_kept(): + for country, required in MAX_LIST.items(): + listed = {p["slug"] for p in PRODUCERS["countries"][country]} + assert required <= listed, required - listed + + +def test_every_producer_is_complete(): + for country, producers in PRODUCERS["countries"].items(): + for p in producers: + assert ( + p["name"] and p["kind"] and p["search_hints"] and p["where_to_look"] + ), p["slug"] + if p["slug"] not in MAX_LIST[country] and p["slug"] != "open_search": + assert p.get("why"), ( + f"{p['slug']} was added beyond Max's list without a reason" + ) + + +def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): + assert ( + cmp.producer_for_source(PRODUCERS, "US", "ca_fiscal_note") + == "state_fiscal_note" + ) + assert cmp.producer_for_source(PRODUCERS, "UK", "obr_efo") == "obr" + assert cmp.producer_for_source(PRODUCERS, "UK", "unlisted_tank") is None + + +# --- the motivating case ------------------------------------------------------------- + + +def test_childcare_gap_fails_the_gate_without_its_explanation(): + record = childcare() + record.annotations = [] + errors = errors_of(record) + unexplained = [e for e in errors if "no annotation explains it" in e] + assert len(unexplained) == 6 + assert any("centax_static_central_fy2027 is -45.8%" in e for e in unexplained) + + +def test_childcare_central_gap_is_what_the_published_figures_imply(): + rows = {r["key"]: r for r in cmp.evaluate(childcare(), POLICY)} + assert rows["centax_static_central_fy2027"]["gap"] == pytest.approx( + 0.434 / 0.80 - 1 + ) + assert rows["centax_static_central_fy2029"]["gap"] == pytest.approx( + 0.505 / 0.98 - 1 + ) + # compared across two worlds, so constructed rather than comparable + assert rows["centax_static_central_fy2027"]["status"] == "constructed" + assert rows["centax_net_central_fy2027"]["estimate"] is None + + +# --- the gate as a property ------------------------------------------------------------- + +money = st.floats(min_value=1e3, max_value=1e12, allow_nan=False, allow_infinity=False) + + +@settings(max_examples=150, deadline=None) +@given(pe=money, ext=money) +def test_gate_fails_exactly_when_an_unexplained_gap_exceeds_the_threshold(pe, ext): + pe, ext = float(round(pe)), float(round(ext)) + gap = pe / ext - 1 + assume(abs(abs(gap) - THRESHOLD) > 1e-9) # stay off the boundary + errors = errors_of(synthetic(-pe, -ext)) + gate_errors = [e for e in errors if "no annotation explains it" in e] + assert [e for e in errors if e not in gate_errors] == [] + assert bool(gate_errors) == (abs(gap) > THRESHOLD) + + +@settings(max_examples=60, deadline=None) +@given(pe=money, ext=money) +def test_an_explained_gap_always_passes(pe, ext): + assert ( + errors_of(synthetic(-float(round(pe)), -float(round(ext)), annotate=True)) == [] + ) + + +def test_an_annotation_that_does_not_explain_the_gap_fails(): + record = synthetic(-100.0, -300.0, annotate=True) + record.annotations[0]["explains_gap"] = False + assert any("explains_gap: true" in e for e in errors_of(record)) + + +# --- the gap rule ---------------------------------------------------------------------- + +nonzero = st.floats(min_value=-1e12, max_value=1e12, allow_nan=False).filter( + lambda x: abs(x) > 1e-3 +) + + +@given(x=nonzero) +def test_gap_is_zero_for_equal_values(x): + assert cmp.relative_gap(x, x, signs.SAME) == 0 + + +@given(pe=nonzero, ext=nonzero, k=st.floats(min_value=1e-3, max_value=1e3)) +def test_gap_is_invariant_to_rescaling_both_values(pe, ext, k): + a = cmp.relative_gap(pe, ext, signs.SAME) + b = cmp.relative_gap(pe * k, ext * k, signs.SAME) + assert math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-9) + + +@given(pe=nonzero, ext=nonzero) +def test_opposite_convention_is_the_value_negated(pe, ext): + assert cmp.relative_gap(pe, -ext, signs.OPPOSITE) == cmp.relative_gap( + pe, ext, signs.SAME + ) + + +@given(pe=nonzero, ext=nonzero) +def test_magnitudes_ignore_sign(pe, ext): + assert cmp.relative_gap(pe, ext, signs.MAGNITUDE) == cmp.relative_gap( + abs(pe), abs(ext), signs.SAME + ) + + +@given(pe=nonzero, ext=nonzero) +def test_opposite_signs_are_always_over_any_threshold_below_one(pe, ext): + assume((pe > 0) != (ext > 0)) + assert cmp.relative_gap(pe, ext, signs.SAME) < -1 + assert cmp.over_threshold(cmp.relative_gap(pe, ext, signs.SAME), 0.999) + + +@given(pe=nonzero, ext=nonzero, t1=st.floats(0.01, 5), t2=st.floats(0.01, 5)) +def test_over_threshold_is_monotone_in_the_threshold(pe, ext, t1, t2): + gap = cmp.relative_gap(pe, ext, signs.SAME) + lo, hi = sorted((t1, t2)) + assert cmp.over_threshold(gap, hi) <= cmp.over_threshold(gap, lo) + + +def test_a_published_zero(): + assert cmp.relative_gap(0.0, 0.0, signs.SAME) == 0 + assert math.isinf(cmp.relative_gap(5.0, 0.0, signs.SAME)) + assert cmp.over_threshold(cmp.relative_gap(5.0, 0.0, signs.SAME), 100) + + +def test_no_rule_no_gap(): + assert cmp.relative_gap(1.0, 1.0, signs.UNKNOWN) is None + assert not cmp.over_threshold(None, 0.25) + + +# --- harvest rule 1 ------------------------------------------------------------------------ + +UNITS = {"m": "GBP m x 1e6 -> GBP", "bn": "GBP bn x 1e9 -> GBP", "": "as printed"} + + +@given( + cents=st.integers(min_value=0, max_value=10**9), + prefix=st.sampled_from(["", "£"]), + unit=st.sampled_from(sorted(UNITS)), + negative=st.booleans(), + minus=st.sampled_from(["-", "−"]), + grouped=st.booleans(), +) +def test_printed_values_parse_back(cents, prefix, unit, negative, minus, grouped): + number = cents / 100 + digits = f"{number:,.2f}" if grouped else f"{number:.2f}" + raw = f"{minus if negative else ''}{prefix}{digits}{unit}" + expected = -number if negative and number else number + assert cmp.parse_printed(raw) == pytest.approx(expected) + row = { + "value": expected * cmp.NORMALIZATIONS[UNITS[unit]], + "value_raw": raw, + "normalization": UNITS[unit], + } + assert cmp.check_value(row, "t") == [] + row["value"] = row["value"] * 1.01 + 1 + assert cmp.check_value(row, "t") != [] + + +@pytest.mark.parametrize( + "raw, value", + [ + ("(0.4)", -0.4), + ("−£1.2 billion", -1.2), + ("2.1 per cent", 2.1), + ("13,360", 13360), + ("+140m", 140), + ], +) +def test_printed_forms(raw, value): + assert cmp.parse_printed(raw) == value + + +def test_a_printed_range_is_two_claims(): + with pytest.raises(ValueError, match="2 numbers"): + cmp.parse_printed("£1.2bn to £1.5bn") + + +def test_a_staged_claim_is_never_re_signed(): + assert cmp.check_value( + {"value": 1, "value_raw": "1", "normalization": "as printed", "sign": -1}, "t" + ) + + +def test_an_estimate_may_be_re_signed_with_a_reason(): + row = { + "value": -434e6, + "value_raw": "0.434", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + } + assert any("sign_basis" in e for e in cmp.check_value(row, "t", signed=True)) + row["sign_basis"] = "printed as a positive cost" + assert cmp.check_value(row, "t", signed=True) == [] + + +# --- each rule fails a record that breaks it ------------------------------------------------- + + +def mutate(fn): + record = synthetic(-100.0, -105.0) + assert errors_of(record) == [] + fn(record) + return errors_of(record) + + +def _search(record): + return record.search_log["searches"][0]["producers"] + + +def test_a_skipped_producer_fails(): + errors = mutate(lambda r: _search(r).pop(0)) + assert any("producers not searched: ['obr']" in e for e in errors) + + +def test_an_unlisted_producer_fails(): + errors = mutate( + lambda r: _search(r).append( + { + "producer": "made_up", + "result": "none_found", + "queries": ["q"], + "claims": [], + } + ) + ) + assert any("not on the UK list" in e for e in errors) + + +def test_not_applicable_needs_the_scope_to_exclude_the_producer(): + def f(r): + _search(r)[0].update(result="not_applicable", queries=[]) + + assert any("applies to 'uk_wide'" in e for e in mutate(f)) + + +def test_a_search_with_nothing_run_fails(): + def f(r): + _search(r)[0].update(queries=[], checked=[]) + + assert any("no query run" in e for e in mutate(f)) + + +def test_found_not_staged_says_what_and_where(): + def f(r): + _search(r)[0].update(result="found_not_staged", note="") + + errors = mutate(f) + assert any("lists the pages it was seen on" in e for e in errors) + assert any("says what was found" in e for e in errors) + + +def test_a_claim_no_search_found_fails(): + def f(r): + for p in _search(r): + p.update(claims=[], result="none_found") if p["producer"] == "ifs" else None + + assert any("no search entry lists this claim" in e for e in mutate(f)) + + +def test_a_listed_producer_finds_only_its_own_claims(): + errors = mutate(lambda r: r.claims[0].update(source="resolution_foundation")) + assert any("is not ifs's" in e for e in errors) + + +def test_a_value_that_is_not_what_was_printed_fails(): + errors = mutate(lambda r: r.claims[0].update(value=-999.0)) + assert any("is not value_raw" in e for e in errors) + + +def test_a_quote_without_the_value_fails(): + errors = mutate(lambda r: r.claims[0].update(quote="no number here")) + assert any("quote does not contain" in e for e in errors) + + +def test_an_unhashed_primary_fails(): + errors = mutate( + lambda r: r.claims[0]["publication"].update(primary_sha256="c" * 64) + ) + assert any("not in manifest.jsonl" in e for e in errors) + + +def test_a_different_world_needs_a_reason(): + def f(r): + r.analysis["worlds"]["other"] = { + "policy": "uk_other", + "description": "another world here", + } + r.claims[0]["world"] = "other" + + assert any("compare across worlds only with" in e for e in mutate(f)) + + +def test_a_different_quantity_is_not_compared(): + errors = mutate(lambda r: r.claims[0].update(period=2029)) + assert any("period 2029 differs" in e for e in errors) + + +def test_an_unreadable_sign_convention_fails(): + errors = mutate( + lambda r: r.claims[0]["conditions"].update( + sign_convention="negative = cost to the Exchequer" + ) + ) + assert any("does not orient the value" in e for e in errors) + + +def test_an_uncompared_claim_says_why(): + errors = mutate(lambda r: r.claims[0].update(compare_to=None)) + assert any("pe_missing must say why" in e for e in errors) + + +def test_a_restated_figure_is_not_compared(): + errors = mutate(lambda r: r.claims[0].update(attribution="restated")) + assert any("only a producer's own estimate is compared" in e for e in errors) + + +def test_an_unregistered_baseline_fails(): + errors = mutate( + lambda r: r.claims[0].update(baseline_policy="not_a_registered_world") + ) + assert any("is not registered" in e for e in errors) + + +def test_an_administrative_level_belongs_to_a_producer_source(): + def f(r): + r.claims[0].update(benchmark_class="administrative_fact", world="baseline") + + assert any("calibration material" in e for e in mutate(f)) + + +def test_an_annotation_without_a_basis_fails(): + def f(r): + r.annotations.append( + { + "id": "x", + "claims": [r.claims[0]["key"]], + "status": "comparable", + "explains_gap": True, + "cause": "a cause long enough to count", + "basis": [], + } + ) + + assert any("no fabricated mechanisms" in e for e in mutate(f)) + + +def test_an_issue_basis_is_a_github_issue(): + def f(r): + r.annotations.append( + { + "id": "x", + "claims": [r.claims[0]["key"]], + "status": "comparable", + "explains_gap": True, + "cause": "a cause long enough to count", + "basis": [{"kind": "issue", "ref": "https://example.org/ticket/1"}], + } + ) + + assert any("GitHub issue or pull URL" in e for e in mutate(f)) + + +def test_pending_is_only_for_what_a_reharvest_found(): + def f(r): + r.annotations.append( + { + "id": "x", + "claims": [r.claims[0]["key"]], + "status": "pending", + "explains_gap": False, + "cause": "found after publication; being explained", + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/2", + } + ], + "tracking_issue": "https://github.com/PolicyEngine/test/issues/2", + } + ) + + assert any("explained before the analysis is recorded" in e for e in mutate(f)) + + +def test_a_reharvest_find_may_wait_on_its_issue(): + record = synthetic(-100.0, -300.0) + first = copy.deepcopy(record.search_log["searches"][0]) + for p in first["producers"]: + if p["producer"] == "ifs": + p.update(result="none_found", claims=[]) + later = record.search_log["searches"][0] + later.update(date="2026-11-02", kind="reharvest") + record.search_log["searches"] = [first, later] + record.claims[0]["found_on"] = "2026-11-02" + assert any("no annotation explains it" in e for e in errors_of(record)) + record.annotations.append( + { + "id": "new-find", + "claims": [record.claims[0]["key"]], + "status": "pending", + "explains_gap": False, + "cause": "found by the weekly re-harvest; the owner is explaining it", + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/3", + } + ], + "tracking_issue": "https://github.com/PolicyEngine/test/issues/3", + } + ) + assert errors_of(record) == [] + entry = cmp.index_entry(record, POLICY) + assert entry["pending"] == 1 + assert entry["tracking_issues"] == ["https://github.com/PolicyEngine/test/issues/3"] + report = cmp.report(record, "2026-11-01", POLICY) + assert report["new_claims"] == ["ext_cost_fy2027"] + assert report["over_threshold"][0]["tracking_issue"].endswith("/3") + + +def test_the_first_search_is_never_a_reharvest(): + errors = mutate(lambda r: r.search_log["searches"][0].update(kind="reharvest")) + assert any("prepublish or backfill" in e for e in errors) + + +# --- the app-v2 record ------------------------------------------------------------------------ + + +@pytest.mark.parametrize("directory", cmp.analysis_dirs(), ids=lambda d: d.name) +def test_app_record_and_index_cite_the_same_search(directory): + record = cmp.load_analysis(directory) + block = cmp.app_record(record) + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] + search = next(s for s in entry["searches"] if s["date"] == block["searchDate"]) + assert block["scorecardSource"] == directory.name + assert block["producersSearched"] == search["producers_searched"] + assert block["found"] == search["found"] + assert block["searchKind"] == search["kind"] + assert block["searchLog"] == entry["search_log"] + + +def test_scaffold_lists_every_producer(tmp_path): + directory = cmp.scaffold("uk", "app", "new-dashboard", SEARCH_DATE, root=tmp_path) + log = json.loads((directory / "search_log.json").read_text()) + logged = [p["producer"] for p in log["searches"][0]["producers"]] + assert logged == [ + p["slug"] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE) + ] + assert directory.name == "analysis-uk-new-dashboard" + + +def test_due_lists_recent_estimates_only(tmp_path): + apps = [ + { + "slug": "a", + "countryId": "uk", + "date": "2026-10-12 12:00:00", + "displayWithResearch": True, + "tags": ["uk", "policy"], + }, + { + "slug": "old", + "countryId": "uk", + "date": "2026-01-01", + "displayWithResearch": True, + "tags": ["uk", "policy"], + }, + { + "slug": "tool", + "countryId": "us", + "date": "2026-10-01", + "displayWithResearch": True, + "tags": ["us"], + }, + {"slug": "unlisted", "countryId": "us", "tags": ["us", "policy"]}, + ] + posts = [ + {"filename": "Some_Post.md", "date": "2026-10-02", "tags": ["us", "policy"]}, + { + "filename": "event.md", + "date": "2026-10-03", + "tags": ["uk", "event"], + "comparables": {"status": "no_estimate", "reason": "x"}, + }, + ] + rows = cmp.due(apps, posts, "2026-10-21", 183, root=tmp_path) + assert [(r["kind"], r["source"]) for r in rows] == [ + ("post", "analysis-us-some-post"), + ("app", "analysis-uk-a"), + ] + + +# --- the database --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def db(): + if not COMMITTED_DB.exists(): + pytest.skip("scorecard.db not built") + conn = sqlite3.connect(COMMITTED_DB) + conn.row_factory = sqlite3.Row + yield conn + conn.close() + + +def _analysis_claims(db): + return db.execute( + "SELECT * FROM external_scores WHERE json_extract(publication, '$.registry') = ?", + ("analysis_comparables",), + ).fetchall() + + +def test_every_analysis_claim_is_held_out(db): + rows = _analysis_claims(db) + assert rows + assert {r["calibration_relationship"] for r in rows} == {"held_out"} + + +def test_db_matches_the_records(db): + expected = {} + for directory in cmp.analysis_dirs(): + record = cmp.load_analysis(directory) + for row in cmp.evaluate(record, POLICY): + expected[(record.analysis["id"], row["key"])] = row + rows = _analysis_claims(db) + seen = set() + for r in rows: + cond = json.loads(r["conditions"]) + key = (cond["analysis"], cond["analysis_claim"]) + seen.add(key) + results = db.execute( + "SELECT status, computed_value FROM pe_results WHERE claim_id = ?", + (r["claim_id"],), + ).fetchall() + if expected[key]["estimate"]: + assert len(results) == 1 + want = expected[key]["status"] + assert results[0]["status"] == ("comparable" if want == "pending" else want) + else: + assert results == [] + assert cond["pe_missing"] + staged = {k for k, row in expected.items()} + assert seen <= staged + + +def test_childcare_result_is_the_published_figure(db): + row = db.execute( + "SELECT r.computed_value, r.run_id, s.value FROM external_scores s JOIN pe_results r" + " ON r.claim_id = s.claim_id WHERE json_extract(s.conditions, '$.analysis_claim') = ?", + ("centax_static_central_fy2027",), + ).fetchone() + assert row["computed_value"] == -434e6 + assert row["value"] == -800e6 + assert row["run_id"] == "analysis:uk/childcare-100k-threshold@f25f0db" + + +@pytest.mark.parametrize("directory", cmp.analysis_dirs(), ids=lambda d: d.name) +def test_index_comparisons_are_every_staged_estimate(directory): + record = cmp.load_analysis(directory) + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] + rows = {r["key"]: r for r in cmp.evaluate(record, POLICY)} + assert [c["claim"] for c in entry["comparisons"]] == [ + c["key"] for c in record.claims + ] + for c in entry["comparisons"]: + row = rows[c["claim"]] + assert c["estimate"] == row["estimate"] + if row["gap"] is not None and not math.isinf(row["gap"]): + assert c["gap"] == pytest.approx(row["gap"], abs=5e-5) + assert (c["pe_value"] is None) == (row["estimate"] is None) + assert (c["pe_missing"] is None) == (row["estimate"] is not None) + if row["over"]: + assert c["cause"], f"{c['claim']} is over the threshold with no cause" diff --git a/tests/test_export_populations.py b/tests/test_export_populations.py index 41c94d3a2..94111225a 100644 --- a/tests/test_export_populations.py +++ b/tests/test_export_populations.py @@ -406,7 +406,10 @@ def test_integration_committed_db(tmp_path): # and FY2025) with CY2024 counterparts (#176). # The March 2026 PMD contributes only its 246 new cells; November's # historical/comprehensive claims and the existing main feed stay intact. - assert dist == {"US": 535, "UK": 5163 + 23_752 + 246, "BE": 9, "NZ": 12} + # Pre-publish comparables (docs/COMPARABLES.md) add every staged claim of + # each analysis record: 20 for the £100,000 childcare dashboard (12 CenTax, + # 8 Centre for British Progress), with or without a result. + assert dist == {"US": 535, "UK": 5163 + 23_752 + 246 + 20, "BE": 9, "NZ": 12} nz = [row for row in payload["rows"] if row["country"] == "NZ"] assert len(nz) == 12 assert {row["source"] for row in nz} == {"nz_treasury"} @@ -441,7 +444,8 @@ def test_integration_committed_db(tmp_path): assert committed["summary"] == payload["summary"] assert committed["event_archives"] == payload["event_archives"] # Every event is reachable in the deployed copy, with bounded requests. - assert len(payload["rows"]) == 5719 + # 5,719 + the 20 pre-publish comparables rows of the childcare record + assert len(payload["rows"]) == 5719 + 20 for archive in payload["event_archives"]: relative = archive["path"].removeprefix("data/") freshly_exported = (tmp_path / relative).read_bytes() From d5bf0ada5007c58daae947b52b238f10b1997847 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 11:36:40 -0400 Subject: [PATCH 03/15] Note the childcare dashboard's takedown in its comparables record Co-Authored-By: Claude Opus 5.5 --- .../README.md | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 sources/analysis-uk-childcare-100k-threshold/README.md diff --git a/sources/analysis-uk-childcare-100k-threshold/README.md b/sources/analysis-uk-childcare-100k-threshold/README.md new file mode 100644 index 000000000..ec64e2202 --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/README.md @@ -0,0 +1,25 @@ +# £100,000 childcare dashboard: comparables record + +The first record under the pre-publish comparables gate (docs/COMPARABLES.md). It is a +backfill: the dashboard was published on 12 October 2026, before the gate existed. + +- **Published figure** (pinned in `analysis.json`): the static cost of the 30 funded hours, £0.434bn + in 2027-28 and £0.505bn in 2029-30, from `PolicyEngine/uk-childcare-100k-threshold` at + `f25f0db` (`data/results.json`). +- **What the search found** (2026-10-21, every UK producer): CenTax's Table 4.2 (static and net, + three scenarios) and the Centre for British Progress's net fiscal impact. DfE's FOI estimate + and the Conservative Party's £700m are logged as found but not staged, because neither has a + readable primary. +- **The gap**: CenTax's central static cost is 46% (2027-28) and 49% (2029-30) above the + published figure. The annotation cites microcosm#1126, the Microcosm hours draw that put + families' use of the extended hours at about 15 of 30, and the dashboard's rerun on the + corrected draw (uk-childcare-100k-threshold#8). +- **Status**: the dashboard was taken off policyengine.org on 21 October 2026 + (PolicyEngine/policyengine-app-v2#1379) while its costing is revised. The record stays: the + miss stays on the page. A republished version needs its own prepublish search appended to + `search_log.json`. Its new figures are pinned as further `pe_estimates`, and the + re-harvest picks the dashboard up again once it is back in `apps.json`. + +The same CenTax and CBP publications are staged at producer level in +`sources/centax-childcare-100k/` and `sources/cbp-rates-and-wrongs/` (#207), with +PolicyEngine's certified-pair counterpart. From 372b256fac0310a4fe6edf018dd23e36bc21be1e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 11:43:38 -0400 Subject: [PATCH 04/15] Refuse a claim on a published quantity that is left uncompared An uncompared claim that shares metric, unit, period and time basis with one of the analysis's estimates must name it (nearest_estimate) and carry a concept_mismatch or pe_gap annotation with a basis. It then sits on the scorecard beside that estimate, labelled, instead of blank. Closes the way a record could dodge an embarrassing comparison with a one-line pe_missing. The childcare record now pins all four published years and annotates the net-of-behaviour CenTax and CBP figures as a concept mismatch. Co-Authored-By: Claude Opus 5.5 --- app/public/data/lanes.json | 4 +- app/public/data/populations.json | 662 ++++++++++++------ data/comparables/index.json | 96 ++- data/lanes.json | 4 +- data/populations.json | 662 ++++++++++++------ docs/COMPARABLES.md | 6 +- scorecard_db/comparables.py | 42 ++ scorecard_db/ingest_analysis_comparables.py | 12 +- .../analysis.json | 132 ++++ .../annotations.json | 36 + .../claims_staged.jsonl | 28 +- tests/test_comparables.py | 72 +- 12 files changed, 1295 insertions(+), 461 deletions(-) diff --git a/app/public/data/lanes.json b/app/public/data/lanes.json index efb8da0ae..cdef6030b 100644 --- a/app/public/data/lanes.json +++ b/app/public/data/lanes.json @@ -587,7 +587,7 @@ "stage": "computed", "running": false, "updated": "2026-10-21", - "note": "1 analyses searched, 20 claims, 6 compared with the analysis's own estimate" + "note": "1 analyses searched, 20 claims, 20 with the analysis's own figure beside them" }, { "id": "analysis-comparables-us", @@ -598,7 +598,7 @@ "stage": "registered", "running": false, "updated": "2026-10-21", - "note": "0 analyses searched, 0 claims, 0 compared with the analysis's own estimate" + "note": "0 analyses searched, 0 claims, 0 with the analysis's own figure beside them" } ] } diff --git a/app/public/data/populations.json b/app/public/data/populations.json index bf1996a00..9db108bfb 100644 --- a/app/public/data/populations.json +++ b/app/public/data/populations.json @@ -108,9 +108,9 @@ }, "by_latest_status": { "comparable": 42, - "concept_mismatch": 17, + "concept_mismatch": 31, "constructed": 1668, - "not_computed": 27121, + "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { @@ -136,9 +136,9 @@ }, "UK": { "claims": 29181, - "with_result": 2168, - "with_value": 1183, - "without_result": 27013, + "with_result": 2182, + "with_value": 1197, + "without_result": 26999, "multi_release_claims": 0 }, "US": { @@ -24929,21 +24929,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.8346153846153846, + "delta": 86000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -24991,7 +25009,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25009,7 +25027,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25062,21 +25080,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.7114754098360656, + "delta": 176000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25124,7 +25160,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25142,7 +25178,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25195,21 +25231,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -3.1, + "delta": -574000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25257,7 +25311,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25275,7 +25329,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25328,21 +25382,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.7890625, + "delta": 135000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25390,7 +25462,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25408,7 +25480,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25461,21 +25533,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.6558441558441559, + "delta": 265000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25523,7 +25613,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25541,7 +25631,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25594,21 +25684,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -3.15625, + "delta": -665000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25656,7 +25764,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25674,7 +25782,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -26647,27 +26755,45 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.690909090909091, + "delta": -888000000.0 }, - "results": [], - "diagnosis": null - }, - { - "claim_id": "f40fe6519a80e65479f2", - "source": "centre_for_british_progress", - "country": "UK", + "results": [ + { + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], + "diagnosis": null + }, + { + "claim_id": "f40fe6519a80e65479f2", + "source": "centre_for_british_progress", + "country": "UK", "source_column": "Abolish Cliff, 2026/27", "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "window": "", @@ -26705,21 +26831,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.9714285714285715, + "delta": -628000000.0 }, - "results": [], + "results": [ + { + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26763,21 +26907,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.6991404011461317, + "delta": -942000000.0 }, - "results": [], + "results": [ + { + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26821,21 +26983,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.701298701298701, + "delta": -670000000.0 }, - "results": [], + "results": [ + { + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26879,21 +27059,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.6340852130325814, + "delta": -1051000000.0 }, - "results": [], + "results": [ + { + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26937,21 +27135,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.164835164835165, + "delta": -743000000.0 }, - "results": [], + "results": [ + { + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26995,21 +27211,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.5878220140515222, + "delta": -1105000000.0 }, - "results": [], + "results": [ + { + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -27053,21 +27287,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -5.136363636363637, + "delta": -810000000.0 }, - "results": [], + "results": [ + { + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { diff --git a/data/comparables/index.json b/data/comparables/index.json index fc0da681e..afbb26545 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -78,6 +78,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.4575, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -99,8 +101,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2027", + "nearest_pe_value": -434000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -120,6 +124,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.4847, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -141,8 +147,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2029", + "nearest_pe_value": -505000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -162,6 +170,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.4707, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -183,8 +193,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2027", + "nearest_pe_value": -434000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -204,6 +216,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.5, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -225,8 +239,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2029", + "nearest_pe_value": -505000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -246,6 +262,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.2517, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -267,8 +285,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2027", + "nearest_pe_value": -434000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -288,6 +308,8 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.2786, + "nearest_estimate": null, + "nearest_pe_value": null, "status": "constructed", "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", "pe_missing": null @@ -309,8 +331,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "thirty_hours_static_fy2029", + "nearest_pe_value": -505000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." }, { @@ -330,8 +354,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2026", + "nearest_pe_value": -558000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -351,8 +377,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2027", + "nearest_pe_value": -593000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -372,8 +400,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2028", + "nearest_pe_value": -652000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -393,8 +423,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2029", + "nearest_pe_value": -678000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -414,8 +446,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2026", + "nearest_pe_value": -558000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -435,8 +469,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2027", + "nearest_pe_value": -593000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -456,8 +492,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2028", + "nearest_pe_value": -652000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." }, { @@ -477,8 +515,10 @@ "estimate_label": null, "pe_value": null, "gap": null, - "status": null, - "cause": null, + "nearest_estimate": "both_schemes_static_fy2029", + "nearest_pe_value": -678000000, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." } ] diff --git a/data/lanes.json b/data/lanes.json index efb8da0ae..cdef6030b 100644 --- a/data/lanes.json +++ b/data/lanes.json @@ -587,7 +587,7 @@ "stage": "computed", "running": false, "updated": "2026-10-21", - "note": "1 analyses searched, 20 claims, 6 compared with the analysis's own estimate" + "note": "1 analyses searched, 20 claims, 20 with the analysis's own figure beside them" }, { "id": "analysis-comparables-us", @@ -598,7 +598,7 @@ "stage": "registered", "running": false, "updated": "2026-10-21", - "note": "0 analyses searched, 0 claims, 0 compared with the analysis's own estimate" + "note": "0 analyses searched, 0 claims, 0 with the analysis's own figure beside them" } ] } diff --git a/data/populations.json b/data/populations.json index bf1996a00..9db108bfb 100644 --- a/data/populations.json +++ b/data/populations.json @@ -108,9 +108,9 @@ }, "by_latest_status": { "comparable": 42, - "concept_mismatch": 17, + "concept_mismatch": 31, "constructed": 1668, - "not_computed": 27121, + "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { @@ -136,9 +136,9 @@ }, "UK": { "claims": 29181, - "with_result": 2168, - "with_value": 1183, - "without_result": 27013, + "with_result": 2182, + "with_value": 1197, + "without_result": 26999, "multi_release_claims": 0 }, "US": { @@ -24929,21 +24929,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.8346153846153846, + "delta": 86000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -24991,7 +25009,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25009,7 +25027,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25062,21 +25080,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.7114754098360656, + "delta": 176000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25124,7 +25160,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25142,7 +25178,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25195,21 +25231,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -3.1, + "delta": -574000000.0 }, - "results": [], + "results": [ + { + "value": -434000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25257,7 +25311,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25275,7 +25329,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25328,21 +25382,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.7890625, + "delta": 135000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25390,7 +25462,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25408,7 +25480,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25461,21 +25533,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.6558441558441559, + "delta": 265000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25523,7 +25613,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25541,7 +25631,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25594,21 +25684,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -3.15625, + "delta": -665000000.0 }, - "results": [], + "results": [ + { + "value": -505000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -25656,7 +25764,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -25674,7 +25782,7 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ @@ -26647,27 +26755,45 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.690909090909091, + "delta": -888000000.0 }, - "results": [], - "diagnosis": null - }, - { - "claim_id": "f40fe6519a80e65479f2", - "source": "centre_for_british_progress", - "country": "UK", + "results": [ + { + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], + "diagnosis": null + }, + { + "claim_id": "f40fe6519a80e65479f2", + "source": "centre_for_british_progress", + "country": "UK", "source_column": "Abolish Cliff, 2026/27", "name": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "window": "", @@ -26705,21 +26831,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.9714285714285715, + "delta": -628000000.0 }, - "results": [], + "results": [ + { + "value": -558000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26763,21 +26907,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.6991404011461317, + "delta": -942000000.0 }, - "results": [], + "results": [ + { + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26821,21 +26983,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.701298701298701, + "delta": -670000000.0 }, - "results": [], + "results": [ + { + "value": -593000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26879,21 +27059,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.6340852130325814, + "delta": -1051000000.0 }, - "results": [], + "results": [ + { + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26937,21 +27135,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -7.164835164835165, + "delta": -743000000.0 }, - "results": [], + "results": [ + { + "value": -652000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -26995,21 +27211,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -1.5878220140515222, + "delta": -1105000000.0 }, - "results": [], + "results": [ + { + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { @@ -27053,21 +27287,39 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", "policyengine_variables": [], - "computed_at": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": -5.136363636363637, + "delta": -810000000.0 }, - "results": [], + "results": [ + { + "value": -678000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], "diagnosis": null }, { diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index b552b416a..c3ff436ab 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -88,7 +88,11 @@ mechanically where they can be: A claim either names the PolicyEngine estimate it is compared with (`compare_to`), or says in `pe_missing` why there is none. A compared claim has the estimate's metric, unit, period and time -basis. When the two score different policy worlds, `compare_to.across_worlds` gives the reason +basis. A claim that has all four in common with a published estimate but is not compared +cannot be waved off with a sentence. It names that estimate in `nearest_estimate`, and an +annotation with status `concept_mismatch` (or `pe_gap`) and a basis says why the two differ: a +net-of-behaviour cost beside a static one, for example. On the scorecard it then appears beside +that estimate, labelled with the mismatch, rather than blank. When the two score different policy worlds, `compare_to.across_worlds` gives the reason the quantities match and its basis, and the comparison is `constructed`, not `comparable`. The gap is PolicyEngine's value over the external value, minus one, with the external value in diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 3225b8d80..3cf42ded4 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -98,6 +98,13 @@ # Searches that may find a producer not on the list; the claim then names # that producer's own slug. OPEN_FINDERS = ("open_search", "academic") +# A claim and an estimate on the same quantity agree on these; an uncompared +# claim that matches one must say why in an annotation with one of these. +MATCH_FIELDS = ("metric", "unit_concept", "period", "time_basis") +MISMATCH_STATUSES = ( + ComparisonStatus.CONCEPT_MISMATCH.value, + ComparisonStatus.PE_GAP.value, +) _DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") _DATETIME = re.compile( @@ -660,6 +667,7 @@ def validate( # claims ---------------------------------------------------------------------- labels = _baseline_labels() claims_by_key: dict[str, dict] = {} + must_annotate: list[str] = [] for i, c in enumerate(record.claims): key = c.get("key") where = f"{name}: claim {key or i + 1}" @@ -796,6 +804,27 @@ def validate( if compare is None: if not _nonempty_str(c.get("pe_missing"), 10): errors.append(f"{where}: not compared, so pe_missing must say why") + # A claim on the same quantity as a published estimate cannot be + # waved off with a sentence: it names that estimate, and an + # annotation with a basis says why the two are not like for like. + matching = sorted( + e["key"] + for e in estimates.values() + if all(c.get(f) == e.get(f) for f in MATCH_FIELDS) + ) + nearest = c.get("nearest_estimate") + if matching and nearest not in matching: + errors.append( + f"{where}: matches {', '.join(matching)} on metric, unit, period " + "and time basis; compare it (compare_to), or name it in " + "nearest_estimate and annotate why they differ in concept" + ) + elif not matching and nearest is not None: + errors.append( + f"{where}: nearest_estimate {nearest!r} does not match this claim" + ) + elif matching: + must_annotate.append(key) continue if c.get("pe_missing") is not None: errors.append(f"{where}: compared, so pe_missing must be absent") @@ -904,6 +933,14 @@ def validate( "explained before the analysis is recorded, not left pending" ) + for key in must_annotate: + note = annotated.get(key) + if note is None or note.get("status") not in MISMATCH_STATUSES: + errors.append( + f"{name}: claim {key} is not compared with its nearest estimate, so an " + f"annotation with status {' or '.join(MISMATCH_STATUSES)} must say why" + ) + # the gate ------------------------------------------------------------------------ if errors: return errors @@ -943,6 +980,7 @@ def evaluate(record: AnalysisRecord, policy: dict | None = None) -> list[dict]: "source": c["source"], "found_on": c["found_on"], "estimate": None, + "nearest": c.get("nearest_estimate"), "relation": None, "gap": None, "over": False, @@ -1065,6 +1103,10 @@ def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: "estimate_label": est["label"] if est else None, "pe_value": est["value"] if est else None, "gap": _round(row["gap"]), + "nearest_estimate": row["nearest"], + "nearest_pe_value": estimates[row["nearest"]]["value"] + if row["nearest"] + else None, "status": row["status"], "cause": causes.get(row["annotation"]), "pe_missing": c.get("pe_missing"), diff --git a/scorecard_db/ingest_analysis_comparables.py b/scorecard_db/ingest_analysis_comparables.py index 7e10cb91b..0f582b942 100644 --- a/scorecard_db/ingest_analysis_comparables.py +++ b/scorecard_db/ingest_analysis_comparables.py @@ -183,12 +183,17 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] scores.append(score) t["claims"] += 1 row = evaluation[c["key"]] - if not row["estimate"]: + if not row["estimate"] and not row["nearest"]: t["not_compared"] += 1 continue - est = estimates[row["estimate"]] + # A compared claim carries the analysis's figure with its record's + # status; an uncompared claim on the same quantity carries its + # nearest estimate, labelled with the mismatch its annotation states. + est = estimates[row["estimate"] or row["nearest"]] note = notes.get(c["key"]) status = row["status"] + if not row["estimate"]: + t["not_compared"] += 1 annotations = [] if note: annotations.append(f"{note['id']}: {note['cause']}") @@ -218,6 +223,7 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] "sha256": prov["sha256"], "field": prov["field"], "relation": row["relation"], + "compared": bool(row["estimate"]), }, sort_keys=True, ), @@ -283,7 +289,7 @@ def ingest(db_path: Path) -> dict: ) detail = ( f"{t['analyses']} analyses searched, {t['claims']} claims, " - f"{t['results']} compared with the analysis's own estimate" + f"{t['results']} with the analysis's own figure beside them" ) db.conn.execute(LANE_SQL, (lane, stage_name, detail, updated)) sync_lane_feed(db, REPO / "data" / "lanes.json", updated, lanes=LANE_FEED_META) diff --git a/sources/analysis-uk-childcare-100k-threshold/analysis.json b/sources/analysis-uk-childcare-100k-threshold/analysis.json index cce20138e..4252a9034 100644 --- a/sources/analysis-uk-childcare-100k-threshold/analysis.json +++ b/sources/analysis-uk-childcare-100k-threshold/analysis.json @@ -33,6 +33,39 @@ } }, "pe_estimates": [ + { + "key": "thirty_hours_static_fy2026", + "label": "Static cost of the 30 funded hours, both limits removed, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -410000000, + "value_raw": "0.41", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, { "key": "thirty_hours_static_fy2027", "label": "Static cost of the 30 funded hours, both limits removed, 2027-28", @@ -66,6 +99,39 @@ "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" } }, + { + "key": "thirty_hours_static_fy2028", + "label": "Static cost of the 30 funded hours, both limits removed, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -484000000, + "value_raw": "0.484", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, { "key": "thirty_hours_static_fy2029", "label": "Static cost of the 30 funded hours, both limits removed, 2029-30", @@ -99,6 +165,39 @@ "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" } }, + { + "key": "both_schemes_static_fy2026", + "label": "Static cost of both schemes, both limits removed, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -558000000, + "value_raw": "0.558", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, { "key": "both_schemes_static_fy2027", "label": "Static cost of both schemes, both limits removed, 2027-28", @@ -132,6 +231,39 @@ "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" } }, + { + "key": "both_schemes_static_fy2028", + "label": "Static cost of both schemes, both limits removed, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -652000000, + "value_raw": "0.652", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, { "key": "both_schemes_static_fy2029", "label": "Static cost of both schemes, both limits removed, 2029-30", diff --git a/sources/analysis-uk-childcare-100k-threshold/annotations.json b/sources/analysis-uk-childcare-100k-threshold/annotations.json index d71c1031c..2141d2890 100644 --- a/sources/analysis-uk-childcare-100k-threshold/annotations.json +++ b/sources/analysis-uk-childcare-100k-threshold/annotations.json @@ -26,5 +26,41 @@ "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/pull/8" } ] + }, + { + "id": "net-of-behaviour-vs-static", + "claims": [ + "centax_net_central_fy2027", + "centax_net_central_fy2029", + "centax_net_high_cost_fy2027", + "centax_net_high_cost_fy2029", + "centax_net_low_cost_fy2027", + "centax_net_low_cost_fy2029", + "cbp_net_table2_fy2026", + "cbp_net_table2_fy2027", + "cbp_net_table2_fy2028", + "cbp_net_table2_fy2029", + "cbp_net_table3_fy2026", + "cbp_net_table3_fy2027", + "cbp_net_table3_fy2028", + "cbp_net_table3_fy2029" + ], + "status": "concept_mismatch", + "explains_gap": false, + "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", + "basis": [ + { + "kind": "document", + "ref": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf#page=42" + }, + { + "kind": "document", + "ref": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf#page=8" + }, + { + "kind": "engine_metadata", + "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/blob/f25f0dbd46c31d336bde06898d801a1887a8cf89/data/results.json" + } + ] } ] diff --git a/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl index 40f554092..2033550a9 100644 --- a/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl +++ b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl @@ -1,20 +1,20 @@ {"key": "centax_static_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -800000000, "value_raw": "-800m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Static Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -520000000, "value_raw": "-520m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_net_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -520000000, "value_raw": "-520m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} {"key": "centax_static_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -980000000, "value_raw": "-980m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Static Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -640000000, "value_raw": "-640m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_net_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -640000000, "value_raw": "-640m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} {"key": "centax_static_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -820000000, "value_raw": "-820m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -610000000, "value_raw": "-610m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_net_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -610000000, "value_raw": "-610m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} {"key": "centax_static_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -1010000000, "value_raw": "-1,010m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -770000000, "value_raw": "-770m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_net_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -770000000, "value_raw": "-770m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} {"key": "centax_static_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -580000000, "value_raw": "-580m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 140000000, "value_raw": "+140m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "centax_net_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 140000000, "value_raw": "+140m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} {"key": "centax_static_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -700000000, "value_raw": "-700m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 160000000, "value_raw": "+160m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "cbp_net_table2_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 70000000, "value_raw": "0.070", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 77000000, "value_raw": "0.077", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 91000000, "value_raw": "0.091", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 132000000, "value_raw": "0.132", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 330000000, "value_raw": "0.330", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 349000000, "value_raw": "0.349", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 399000000, "value_raw": "0.399", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 427000000, "value_raw": "0.427", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "centax_net_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 160000000, "value_raw": "+160m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} +{"key": "cbp_net_table2_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 70000000, "value_raw": "0.070", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2026", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 77000000, "value_raw": "0.077", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2027", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 91000000, "value_raw": "0.091", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2028", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 132000000, "value_raw": "0.132", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2029", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 330000000, "value_raw": "0.330", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2026", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 349000000, "value_raw": "0.349", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2027", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 399000000, "value_raw": "0.399", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2028", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 427000000, "value_raw": "0.427", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2029", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 4c2247b68..5979b859d 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -297,7 +297,9 @@ def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): def test_childcare_gap_fails_the_gate_without_its_explanation(): record = childcare() - record.annotations = [] + record.annotations = [ + n for n in record.annotations if n["id"] != "hours-draw-microcosm-1126" + ] errors = errors_of(record) unexplained = [e for e in errors if "no annotation explains it" in e] assert len(unexplained) == 6 @@ -315,6 +317,10 @@ def test_childcare_central_gap_is_what_the_published_figures_imply(): # compared across two worlds, so constructed rather than comparable assert rows["centax_static_central_fy2027"]["status"] == "constructed" assert rows["centax_net_central_fy2027"]["estimate"] is None + # net of behaviour: beside the static figure, labelled a concept mismatch + assert rows["centax_net_central_fy2027"]["nearest"] == "thirty_hours_static_fy2027" + assert rows["centax_net_central_fy2027"]["status"] == "concept_mismatch" + assert rows["cbp_net_table2_fy2029"]["nearest"] == "both_schemes_static_fy2029" # --- the gate as a property ------------------------------------------------------------- @@ -588,6 +594,62 @@ def test_an_uncompared_claim_says_why(): assert any("pe_missing must say why" in e for e in errors) +def test_a_claim_on_a_published_quantity_cannot_be_waved_off(): + def f(r): + r.claims[0].update( + compare_to=None, pe_missing="a different concept, says the stager" + ) + + errors = mutate(f) + assert any("matches pe_cost_fy2027 on metric, unit, period" in e for e in errors) + + +def test_a_waved_off_claim_needs_a_cited_mismatch(): + def name_it(r): + r.claims[0].update( + compare_to=None, + pe_missing="the producer's figure is net of behaviour", + nearest_estimate="pe_cost_fy2027", + ) + + assert any("must say why" in e for e in mutate(name_it)) + + def annotate(r): + name_it(r) + r.annotations.append( + { + "id": "net-vs-static", + "claims": [r.claims[0]["key"]], + "status": "concept_mismatch", + "explains_gap": False, + "cause": "the producer's figure is net of behavioural responses", + "basis": [ + {"kind": "document", "ref": "https://example.org/report.pdf#page=3"} + ], + } + ) + + assert mutate(annotate) == [] + + def comparable_status(r): + annotate(r) + r.annotations[0]["status"] = "comparable" + + assert any("must say why" in e for e in mutate(comparable_status)) + + +def test_nearest_estimate_must_match(): + def f(r): + r.claims[0].update( + compare_to=None, + pe_missing="a different period entirely", + period=2031, + nearest_estimate="pe_cost_fy2027", + ) + + assert any("does not match this claim" in e for e in mutate(f)) + + def test_a_restated_figure_is_not_compared(): errors = mutate(lambda r: r.claims[0].update(attribution="restated")) assert any("only a producer's own estimate is compared" in e for e in errors) @@ -815,6 +877,12 @@ def test_db_matches_the_records(db): assert len(results) == 1 want = expected[key]["status"] assert results[0]["status"] == ("comparable" if want == "pending" else want) + elif expected[key]["nearest"]: + # beside its nearest estimate, labelled with the stated mismatch + assert len(results) == 1 + assert results[0]["status"] == expected[key]["status"] + assert results[0]["status"] in cmp.MISMATCH_STATUSES + assert cond["pe_missing"] else: assert results == [] assert cond["pe_missing"] @@ -847,6 +915,8 @@ def test_index_comparisons_are_every_staged_estimate(directory): if row["gap"] is not None and not math.isinf(row["gap"]): assert c["gap"] == pytest.approx(row["gap"], abs=5e-5) assert (c["pe_value"] is None) == (row["estimate"] is None) + assert c["nearest_estimate"] == row["nearest"] + assert (c["nearest_pe_value"] is None) == (row["nearest"] is None) assert (c["pe_missing"] is None) == (row["estimate"] is not None) if row["over"]: assert c["cause"], f"{c['claim']} is over the threshold with no cause" From b3fb90e500fd0802f8939140e158df32e18c3b19 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 11:47:50 -0400 Subject: [PATCH 05/15] Require a fresh comparables search before an analysis is promoted Max, 2026-10-21: "even if we've already published something, we should do a fresh comp review before big pushes like newsletters". Adds the prepromotion search kind, promotion_window_days (7, queued for Max), comparables.fresh and its CLI, and puts the window and each claim's found_on in the index, which PolicyEngine/newsletters' CI reads for every analysis an edition links. Co-Authored-By: Claude Opus 5.5 --- data/comparables/index.json | 21 +++++++ data/comparables/policy.json | 2 + docs/COMPARABLES.md | 22 ++++++- scorecard_db/comparables.py | 68 +++++++++++++++++++- tests/test_comparables.py | 116 +++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 4 deletions(-) diff --git a/data/comparables/index.json b/data/comparables/index.json index afbb26545..26fc02d4b 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -2,6 +2,7 @@ "_what": "Every PolicyEngine analysis whose comparables search is recorded in this repository, keyed by its sources/ directory. Generated by `python -m scorecard_db.comparables index` from the directories themselves; policyengine-app-v2 reads it to check a research entry's comparables record. A listing means the record passed the gate.", "schema": "comparables/index/1", "gap_threshold": 0.25, + "promotion_window_days": 7, "analyses": { "analysis-uk-childcare-100k-threshold": { "id": "uk/childcare-100k-threshold", @@ -63,6 +64,7 @@ "comparisons": [ { "claim": "centax_static_central_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -86,6 +88,7 @@ }, { "claim": "centax_net_central_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -109,6 +112,7 @@ }, { "claim": "centax_static_central_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -132,6 +136,7 @@ }, { "claim": "centax_net_central_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -155,6 +160,7 @@ }, { "claim": "centax_static_high_cost_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -178,6 +184,7 @@ }, { "claim": "centax_net_high_cost_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -201,6 +208,7 @@ }, { "claim": "centax_static_high_cost_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -224,6 +232,7 @@ }, { "claim": "centax_net_high_cost_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -247,6 +256,7 @@ }, { "claim": "centax_static_low_cost_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -270,6 +280,7 @@ }, { "claim": "centax_net_low_cost_fy2027", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -293,6 +304,7 @@ }, { "claim": "centax_static_low_cost_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -316,6 +328,7 @@ }, { "claim": "centax_net_low_cost_fy2029", + "found_on": "2026-10-21", "source": "centax", "publisher": "CenTax", "title": "Removing the childcare cliff-edge: impacts and cost of reform", @@ -339,6 +352,7 @@ }, { "claim": "cbp_net_table2_fy2026", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -362,6 +376,7 @@ }, { "claim": "cbp_net_table2_fy2027", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -385,6 +400,7 @@ }, { "claim": "cbp_net_table2_fy2028", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -408,6 +424,7 @@ }, { "claim": "cbp_net_table2_fy2029", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -431,6 +448,7 @@ }, { "claim": "cbp_net_table3_fy2026", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -454,6 +472,7 @@ }, { "claim": "cbp_net_table3_fy2027", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -477,6 +496,7 @@ }, { "claim": "cbp_net_table3_fy2028", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", @@ -500,6 +520,7 @@ }, { "claim": "cbp_net_table3_fy2029", + "found_on": "2026-10-21", "source": "centre_for_british_progress", "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", diff --git a/data/comparables/policy.json b/data/comparables/policy.json index bc72c4f61..f591b6e57 100644 --- a/data/comparables/policy.json +++ b/data/comparables/policy.json @@ -3,6 +3,8 @@ "gap_threshold": 0.25, "gap_rule": "relative gap = PolicyEngine value / external value in PolicyEngine's orientation - 1 (scorecard_db/signs.py orients the external value; an unsigned magnitude compares absolute values). A gap whose absolute value exceeds gap_threshold needs an annotation that names its cause and cites a basis before the analysis can be recorded as reviewed.", "gap_threshold_status": "default proposed on 2026-10-21, queued for Max's ruling", + "promotion_window_days": 7, + "promotion_rule": "An analysis featured in a newsletter, social campaign, webinar or press push needs a comparables search dated within promotion_window_days before the push, and no gap found by then still pending (comparables.fresh; the newsletters repository enforces it for every analysis an edition links). Max, 2026-10-21; the window is queued for his ruling.", "reharvest_window_days": 183, "reharvest_note": "Published analyses are searched again weekly for this many days after their registry date (the launchd job comparables-reharvest)." } diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index c3ff436ab..14e83e287 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -31,6 +31,26 @@ validator below fails it on six rows. analysis published in the last `reharvest_window_days` (183). A new comparable becomes a row here; a new gap over the threshold also opens an issue for the analysis's owner. +## Before a push + +Publishing is not the only moment that counts. When an analysis that is already published is +featured in a newsletter, a social campaign, a webinar or a press push, its comparables search is +run again first (Max, 2026-10-21: "even if we've already published something, we should do a +fresh comp review before big pushes like newsletters"). The 21 October UK newsletter promoted the +childcare dashboard at £0.59bn. A fresh search that day would have put CenTax's £0.80bn static +cost for the 30 hours alone beside it. + +- The search is appended to the analysis's `search_log.json` with `kind: prepromotion`, or the + analysis's first search if it has none yet (`backfill`). +- The push may go ahead only if a search is dated within `promotion_window_days` + (`data/comparables/policy.json`, 7 by default) before the push date, and no gap found by then + is still `pending`. `python -m scorecard_db.comparables fresh analysis-- --on + ` says yes or why not. +- PolicyEngine/newsletters' CI applies the same rule to every policyengine.org analysis an edition + links, for editions dated on or after its cutover. The index publishes the window + (`promotion_window_days`) and when each claim was found, so that check needs nothing but the + index. + ## The directory `sources/analysis--/`, where `` is the analysis's slug on policyengine.org @@ -39,7 +59,7 @@ validator below fails it on six rows. | File | What it holds | |---|---| | `analysis.json` | The registry entry, scope, search terms, the policy worlds it scores, and every PolicyEngine estimate it publishes, each pinned to its results file by repo, commit, path, field and SHA-256 | -| `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`), who ran it, and one entry per listed producer | +| `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`, `prepromotion`), who ran it, and one entry per listed producer | | `manifest.jsonl` | One line per primary publication, with its SHA-256 | | `claims_staged.jsonl` | Every comparable found, staged under the harvest rules | | `annotations.json` | The status and cause of anything that is not a plain comparison, each citing a basis | diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 3cf42ded4..f830db0a2 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -67,7 +67,16 @@ INDEX_SCHEMA = "comparables/index/1" DIR_PREFIX = "analysis-" -SEARCH_KINDS = ("prepublish", "backfill", "reharvest") +# prepublish: before the analysis is listed; backfill: the first search of an +# analysis listed before the gate; reharvest: the weekly search after +# publication; prepromotion: a fresh search before a newsletter, social +# campaign, webinar or press push (Max, 2026-10-21: "even if we've already +# published something, we should do a fresh comp review before big pushes like +# newsletters"). +SEARCH_KINDS = ("prepublish", "backfill", "reharvest", "prepromotion") +# Searches after publication: what they find may wait on its owner's issue +# (`pending`), but an analysis is never promoted with a pending gap (``fresh``). +POST_PUBLICATION_KINDS = ("reharvest", "prepromotion") # found_not_staged: a comparable exists but its primary cannot be read (harvest # rule 8: a figure whose primary could not be read is never a claim); the note # says what it is and where it was seen. @@ -270,6 +279,10 @@ def load_policy(path: Path = POLICY_PATH) -> dict: raise ValueError( f"{path}: gap_threshold must be a positive number: {threshold!r}" ) + for key in ("promotion_window_days", "reharvest_window_days"): + days = policy.get(key) + if not (isinstance(days, int) and not isinstance(days, bool) and days > 0): + raise ValueError(f"{path}: {key} must be a positive whole number: {days!r}") return policy @@ -593,7 +606,7 @@ def validate( previous_date = date if skind not in SEARCH_KINDS: errors.append(f"{where}: kind {skind!r} not in {SEARCH_KINDS}") - if i == 0 and skind == "reharvest": + if i == 0 and skind in POST_PUBLICATION_KINDS: errors.append( f"{where}: the first search is a prepublish or backfill search" ) @@ -926,7 +939,7 @@ def validate( annotated[key] = n if ( status == "pending" - and claim_found.get(key, ("", ""))[1] != "reharvest" + and claim_found.get(key, ("", ""))[1] not in POST_PUBLICATION_KINDS ): errors.append( f"{where}: {key} was found before publication; its gap is " @@ -1088,6 +1101,7 @@ def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: out.append( { "claim": c["key"], + "found_on": c["found_on"], "source": c["source"], "publisher": c["publication"]["publisher"], "title": c["publication"]["title"], @@ -1143,6 +1157,7 @@ def build_index( ), "schema": INDEX_SCHEMA, "gap_threshold": policy["gap_threshold"], + "promotion_window_days": policy["promotion_window_days"], "analyses": analyses, } @@ -1320,6 +1335,35 @@ def due( return sorted(rows, key=lambda r: (r["date"], r["source"])) +def fresh(entry: dict, on: str, window_days: int) -> list[str]: + """Why an analysis may not be promoted on date ``on`` (a newsletter, social + campaign, webinar or press push), read from its index entry: no search in + the ``window_days`` up to and including ``on``, or a gap found by then that + is still pending. Empty when it may. The newsletters repository applies the + same rule to every analysis an edition links.""" + start = (dt.date.fromisoformat(on) - dt.timedelta(days=window_days)).isoformat() + reasons = [] + recent = [s for s in entry["searches"] if start <= s["date"] <= on] + if not recent: + last = max( + (s["date"] for s in entry["searches"] if s["date"] <= on), default=None + ) + reasons.append( + f"no comparables search between {start} and {on} (last: {last or 'none'}); " + "run a prepromotion search before the push" + ) + pending = [ + c["claim"] + for c in entry.get("comparisons", []) + if c["status"] == "pending" and c["found_on"] <= on + ] + if pending: + reasons.append( + f"gaps still pending by {on}: {', '.join(pending)}; explain them before the push" + ) + return reasons + + def report(record: AnalysisRecord, since: str, policy: dict | None = None) -> dict: """New comparables found on or after ``since`` and every gap over the threshold among them: the text of a re-harvest's issue for the owner.""" @@ -1380,6 +1424,9 @@ def _main(argv: list[str]) -> int: p_report = sub.add_parser("report", help="comparables found since a date") p_report.add_argument("source") p_report.add_argument("--since", required=True) + p_fresh = sub.add_parser("fresh", help="may the analysis be promoted on a date?") + p_fresh.add_argument("source") + p_fresh.add_argument("--on", default=_today()) args = parser.parse_args(argv) if args.cmd == "check": @@ -1446,6 +1493,21 @@ def _main(argv: list[str]) -> int: ) ) return 0 + if args.cmd == "fresh": + record = load_analysis(SOURCES / args.source) + policy = load_policy() + errors = validate(record, policy=policy) + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + reasons = fresh( + index_entry(record, policy), args.on, policy["promotion_window_days"] + ) + for reason in reasons: + print(f"NOT FRESH {args.source}: {reason}") + if not reasons: + print(f"fresh {args.source} on {args.on}") + return 1 if reasons else 0 return 2 diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 5979b859d..bd88a14d2 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -920,3 +920,119 @@ def test_index_comparisons_are_every_staged_estimate(directory): assert (c["pe_missing"] is None) == (row["estimate"] is not None) if row["over"]: assert c["cause"], f"{c['claim']} is over the threshold with no cause" + + +# --- promotion: a fresh search before a push ---------------------------------------- + +WINDOW = POLICY["promotion_window_days"] + + +def _entry(search_dates, pending=()): + return { + "searches": [{"date": d, "kind": "reharvest"} for d in search_dates], + "comparisons": [ + {"claim": f"c{i}", "status": "pending", "found_on": found} + for i, found in enumerate(pending) + ], + } + + +days_st = st.integers(min_value=-400, max_value=400) + + +def _shift(days): + import datetime as dt + + return (dt.date(2026, 11, 2) + dt.timedelta(days=days)).isoformat() + + +@given(offsets=st.lists(days_st, min_size=1, max_size=6), window=st.integers(1, 60)) +def test_fresh_exactly_when_a_search_falls_in_the_window(offsets, window): + on = _shift(0) + dates = sorted(_shift(o) for o in offsets) + in_window = any(-window <= o <= 0 for o in offsets) + assert (cmp.fresh(_entry(dates), on, window) == []) == in_window + + +@given( + offsets=st.lists(days_st, min_size=1, max_size=6), + w1=st.integers(1, 60), + w2=st.integers(1, 60), +) +def test_a_wider_window_is_never_stricter(offsets, w1, w2): + on = _shift(0) + dates = sorted(_shift(o) for o in offsets) + narrow, wide = sorted((w1, w2)) + if cmp.fresh(_entry(dates), on, narrow) == []: + assert cmp.fresh(_entry(dates), on, wide) == [] + + +@given(found=days_st) +def test_a_pending_gap_blocks_a_push_only_once_found(found): + on = _shift(0) + reasons = cmp.fresh(_entry([on], pending=[_shift(found)]), on, WINDOW) + assert bool(reasons) == (found <= 0) + + +def test_a_search_after_the_push_does_not_count(): + assert cmp.fresh(_entry([_shift(1)]), _shift(0), WINDOW) != [] + + +def test_the_childcare_record_was_fresh_on_its_search_day_only_for_the_window(): + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][CHILDCARE.name] + assert cmp.fresh(entry, "2026-10-21", WINDOW) == [] + assert cmp.fresh(entry, f"2026-10-{21 + WINDOW:02d}", WINDOW) == [] + late = cmp.fresh(entry, f"2026-10-{22 + WINDOW:02d}", WINDOW) + assert late and "run a prepromotion search" in late[0] + + +def test_the_index_carries_the_window_and_when_each_claim_was_found(): + index = json.loads(cmp.INDEX_PATH.read_text()) + assert index["promotion_window_days"] == WINDOW + for entry in index["analyses"].values(): + assert all("found_on" in c for c in entry["comparisons"]) + + +def test_a_prepromotion_search_is_never_the_first(): + errors = mutate(lambda r: r.search_log["searches"][0].update(kind="prepromotion")) + assert any("prepublish or backfill" in e for e in errors) + + +def test_a_prepromotion_find_may_wait_on_its_issue_but_blocks_the_push(): + record = synthetic(-100.0, -300.0) + first = copy.deepcopy(record.search_log["searches"][0]) + for p in first["producers"]: + if p["producer"] == "ifs": + p.update(result="none_found", claims=[]) + later = record.search_log["searches"][0] + later.update(date="2026-11-02", kind="prepromotion") + record.search_log["searches"] = [first, later] + record.claims[0]["found_on"] = "2026-11-02" + record.annotations.append( + { + "id": "found-before-a-push", + "claims": [record.claims[0]["key"]], + "status": "pending", + "explains_gap": False, + "cause": "found by the search before the newsletter; the owner is explaining it", + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/4", + } + ], + "tracking_issue": "https://github.com/PolicyEngine/test/issues/4", + } + ) + assert errors_of(record) == [] + entry = cmp.index_entry(record, POLICY) + reasons = cmp.fresh(entry, "2026-11-03", WINDOW) + assert len(reasons) == 1 and "pending" in reasons[0] + + +def test_the_policy_rejects_a_bad_window(tmp_path): + bad = dict(POLICY, promotion_window_days=0) + path = tmp_path / "policy.json" + path.write_text(json.dumps(bad)) + with pytest.raises(ValueError, match="promotion_window_days"): + cmp.load_policy(path) From 22977072ec71e2fc4d62bfc8127f45226e2f78d5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 11:42:12 -0400 Subject: [PATCH 06/15] fixup! Require a fresh comparables search before an analysis is promoted --- scorecard_db/comparables.py | 1280 ++++++++++------ scorecard_db/export_populations.py | 14 +- scorecard_db/ingest_analysis_comparables.py | 124 +- .../analysis.json | 922 +++++++++++- .../annotations.json | 42 +- .../claims_staged.jsonl | 42 +- .../search_log.json | 9 +- tests/test_comparables.py | 1288 ++++++++++------- 8 files changed, 2660 insertions(+), 1061 deletions(-) diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index f830db0a2..cbd7075ab 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -1,5 +1,6 @@ -"""Pre-publish comparables: every PolicyEngine analysis is searched against -every external producer before it is published (Max, 2026-10-21; docs/COMPARABLES.md). +"""Comparables: every PolicyEngine analysis is searched against every external +producer before it is published and again before it is promoted +(docs/COMPARABLES.md). One directory per analysis, ``sources/analysis--/``: @@ -7,36 +8,40 @@ scope, search terms, the policy worlds it scores and the PolicyEngine estimates it publishes, each pinned to the results file it was read from (SHA-256) - search_log.json every search: date, kind, who ran it, and one entry - per producer in data/comparables/producers.json - (found / none_found / not_applicable, the queries run - and the pages checked) + search_log.json every search: date, kind, who ran it, the results + files it compared against, and one entry per producer + in data/comparables/producers.json (found / + none_found / found_not_staged / not_applicable, the + queries run and the pages checked) manifest.jsonl one line per primary publication, hashed claims_staged.jsonl every comparable found, staged under the harvest - rules (sources/harvest-uk-ab2025-2026-09-24/README.md): - verbatim value, registered metric and unit or a - proposal, named baseline, attribution, - benchmark_class; plus the PolicyEngine estimate it is - compared with, or why there is none - annotations.json the cause of every gap over the threshold - (data/comparables/policy.json), each citing a basis - -The validator below is the gate: a directory that fails it fails CI, so -everything on main is a complete record. ``build_index`` writes -data/comparables/index.json, which policyengine-app-v2's CI reads to check -that every research entry published after its cutover names a complete -record here. Nothing in this module reads the network. + rules: the value as printed and normalised, the + quantity it measures (metric, unit, period, basis, + programme, world), attribution and benchmark class; + plus the PolicyEngine estimate it is compared with, + or the nearest one and why they differ + annotations.json the status and cause of anything that is not a plain + comparison within the threshold, each citing a basis + +The validator below is the gate: a directory that fails it fails CI and the +database build, so everything on main is a complete record. ``build_index`` +writes data/comparables/index.json, which policyengine-app-v2's and +PolicyEngine/newsletters' CI read. Nothing in this module reads the network. PYTHONPATH=. python -m scorecard_db.comparables check PYTHONPATH=. python -m scorecard_db.comparables index [--check] PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk \\ --kind app --slug childcare-100k-threshold + PYTHONPATH=. python -m scorecard_db.comparables pins \\ + analysis-uk-childcare-100k-threshold PYTHONPATH=. python -m scorecard_db.comparables record \\ analysis-uk-childcare-100k-threshold PYTHONPATH=. python -m scorecard_db.comparables due \\ --apps apps.json --posts posts.json --today 2026-10-26 PYTHONPATH=. python -m scorecard_db.comparables report \\ analysis-uk-childcare-100k-threshold --since 2026-10-21 + PYTHONPATH=. python -m scorecard_db.comparables fresh \\ + analysis-uk-childcare-100k-threshold --on 2026-10-26 """ from __future__ import annotations @@ -48,6 +53,7 @@ import math import re import sys +from collections import defaultdict from dataclasses import dataclass, field from pathlib import Path @@ -67,16 +73,17 @@ INDEX_SCHEMA = "comparables/index/1" DIR_PREFIX = "analysis-" -# prepublish: before the analysis is listed; backfill: the first search of an -# analysis listed before the gate; reharvest: the weekly search after -# publication; prepromotion: a fresh search before a newsletter, social -# campaign, webinar or press push (Max, 2026-10-21: "even if we've already -# published something, we should do a fresh comp review before big pushes like -# newsletters"). +# prepublish: before the analysis is listed. backfill: the first search of an +# analysis listed before the gate, made after it was published. reharvest: the +# weekly search after publication. prepromotion: a fresh search before a +# newsletter, social campaign, webinar or press push features the analysis. SEARCH_KINDS = ("prepublish", "backfill", "reharvest", "prepromotion") -# Searches after publication: what they find may wait on its owner's issue -# (`pending`), but an analysis is never promoted with a pending gap (``fresh``). -POST_PUBLICATION_KINDS = ("reharvest", "prepromotion") +FIRST_KINDS = ("prepublish", "backfill") +LATER_KINDS = ("reharvest", "prepromotion") +# Searches made after publication. What they find may wait on its owner's +# tracking issue (`pending`); what a prepublish search finds never may, and an +# analysis is never promoted with a pending gap (``fresh``). +POST_PUBLICATION_KINDS = ("backfill", "reharvest", "prepromotion") # found_not_staged: a comparable exists but its primary cannot be read (harvest # rule 8: a figure whose primary could not be read is never a claim); the note # says what it is and where it was seen. @@ -89,33 +96,70 @@ "approx_chart_reading", "cumulative", ) +RANGE_KINDS = ("range_low", "range_high") ATTRIBUTIONS = ("own", "restated", "same_assumptions") BENCHMARK_CLASSES = ("different_model", "administrative_fact", "same_assumptions") PARSE_CONFIDENCE = ("high", "medium", "low") BASIS_KINDS = ("issue", "document", "engine_metadata", "diagnostic") -# An annotation's status is the comparison status its claims carry. "pending" -# is the one extra: a comparable a post-publication re-harvest found, whose -# gap is still being explained on its tracking issue. -ANNOTATION_STATUSES = ( +# The basis of an estimate or claim: what it holds fixed. Required on every +# estimate and claim, and part of what makes two figures the same quantity. +BASES = ("static", "post_behavioural", "forecast", "outturn") +# A claim compared with an estimate has one of these statuses; a mismatch +# means it is not compared, so it never labels a compared claim. +COMPARED_STATUSES = ( ComparisonStatus.COMPARABLE.value, ComparisonStatus.CONSTRUCTED.value, - ComparisonStatus.CONCEPT_MISMATCH.value, ComparisonStatus.PE_GAP.value, "pending", ) +# An uncompared claim beside its nearest estimate carries one of these. +MISMATCH_STATUSES = ( + ComparisonStatus.CONCEPT_MISMATCH.value, + ComparisonStatus.PE_GAP.value, +) +ANNOTATION_STATUSES = tuple(dict.fromkeys(COMPARED_STATUSES + MISMATCH_STATUSES)) BASELINE_WORLD = "baseline" # Searches that may find a producer not on the list; the claim then names # that producer's own slug. OPEN_FINDERS = ("open_search", "academic") -# A claim and an estimate on the same quantity agree on these; an uncompared -# claim that matches one must say why in an annotation with one of these. -MATCH_FIELDS = ("metric", "unit_concept", "period", "time_basis") -MISMATCH_STATUSES = ( - ComparisonStatus.CONCEPT_MISMATCH.value, - ComparisonStatus.PE_GAP.value, +# A claim's source is the producer that published it: never a search, and +# never PolicyEngine, which is not its own comparable. +BANNED_SOURCES = ("open_search", "academic", "policyengine") +# Everything that makes a claim and an estimate the same quantity. +IDENTITY_FIELDS = ("metric", "unit_concept", "period", "time_basis", "world") +IDENTITY_CONDITIONS = ("basis", "program") + +# What makes a policyengine.org entry publish an estimate, and when it may say +# it publishes none. The same rule is in policyengine-app-v2's +# website/src/data/comparables.ts and PolicyEngine/newsletters. +ANALYSIS_TAGS = frozenset( + { + "policy", + "technical", + "autumn-budget", + "tax", + "benefit", + "reconciliation", + "healthcare", + "vat", + "tariffs", + "child-tax-credit", + "behavioral-responses", + "inflation", + "election", + "2024-election", + "benefit-access", + "interactives", + } +) +NO_ESTIMATE_TAGS = frozenset( + {"org", "event", "giving", "conferences", "year-in-review", "april-fools"} ) +COUNTRY_TAGS = ("uk", "us", "ca", "ng", "il") +DEFAULT_COUNTRY = "us" # the website files a post with no country tag under us _DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$") +_FY = re.compile(r"^(\d{4})-(\d{2})$") _DATETIME = re.compile( r"^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?)?$" ) @@ -125,67 +169,113 @@ _SHA256 = re.compile(r"^[0-9a-f]{64}$") _COMMIT = re.compile(r"^[0-9a-f]{40}$") _REPO = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +_PIN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+@[0-9a-f]{40}:[0-9a-f]{64}$") _GITHUB_ISSUE = re.compile( - r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(issues|pull)/\d+(#.*)?$" + r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(issues|pull)/\d+" + r"(#[A-Za-z0-9_-]+)?$" ) _GITHUB_PINNED = re.compile( r"^https://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/(blob|tree)/[0-9a-f]{40}/\S+$" ) _URL = re.compile(r"^https?://\S+$") +# A document or diagnostic URL names a page, not just a host. +_URL_WITH_PATH = re.compile(r"^https://[^/\s]+\.[^/\s]+/\S+$") # Harvest rule 1: ``value`` is ``value_raw`` normalised to the unit, with the -# conversion named. Only these conversions are checked mechanically; a new one -# is added here deliberately, never improvised in a staged row. +# conversion named, and the scale a printed value states must be the one the +# conversion assumes. Only these conversions are checked mechanically; a new +# one is added here deliberately, never improvised in a staged row. NORMALIZATIONS = { - "as printed": 1.0, - "percent as printed": 1.0, - "fraction x 100 -> percent": 100.0, - "percent / 100 -> fraction": 0.01, - "thousands x 1e3": 1e3, - "millions x 1e6": 1e6, - "billions x 1e9": 1e9, - "GBP k x 1e3 -> GBP": 1e3, - "GBP m x 1e6 -> GBP": 1e6, - "GBP bn x 1e9 -> GBP": 1e9, - "USD k x 1e3 -> USD": 1e3, - "USD m x 1e6 -> USD": 1e6, - "USD bn x 1e9 -> USD": 1e9, - "USD tn x 1e12 -> USD": 1e12, + "as printed": (1.0, None), + "percent as printed": (1.0, "pct"), + "fraction x 100 -> percent": (100.0, None), + "percent / 100 -> fraction": (0.01, "pct"), + "thousands x 1e3": (1e3, "k"), + "millions x 1e6": (1e6, "m"), + "billions x 1e9": (1e9, "bn"), + "GBP k x 1e3 -> GBP": (1e3, "k"), + "GBP m x 1e6 -> GBP": (1e6, "m"), + "GBP bn x 1e9 -> GBP": (1e9, "bn"), + "USD k x 1e3 -> USD": (1e3, "k"), + "USD m x 1e6 -> USD": (1e6, "m"), + "USD bn x 1e9 -> USD": (1e9, "bn"), + "USD tn x 1e12 -> USD": (1e12, "tn"), } +_SCALE_WORDS = ( + (re.compile(r"^\s*(?:bn|billion)\b", re.I), "bn"), + (re.compile(r"^\s*(?:tn|trillion)\b", re.I), "tn"), + (re.compile(r"^\s*(?:m|mn|million)\b", re.I), "m"), + (re.compile(r"^\s*(?:k|thousand)\b", re.I), "k"), + (re.compile(r"^\s*(?:%|per\s?cent\b|percent\b|pp\b|percentage points?\b)", re.I), "pct"), +) _NUMBER = re.compile(r"\d+(?:,\d{3})*(?:\.\d+)?|\.\d+") -_MINUS = ("-", "−", "–") +_MINUS = "-−–‒-" +_SIGN_BEFORE = re.compile(r"[" + _MINUS + r"]\s*[£$€]?\s*$") +_MINUS_WORD_BEFORE = re.compile(r"\bminus\s*[£$€]?\s*$", re.I) +_PAREN_BEFORE = re.compile(r"\(\s*[£$€]?\s*$") +_PAREN_AFTER = re.compile( + r"^\s*(?:bn|billion|mn|m|million|k|thousand|tn|trillion|%)?\s*\)", re.I +) + + +def _number_match(value_raw: str) -> re.Match: + if not isinstance(value_raw, str) or not value_raw.strip(): + raise ValueError(f"value_raw must be a nonempty string: {value_raw!r}") + numbers = list(_NUMBER.finditer(value_raw.strip())) + if len(numbers) != 1: + raise ValueError( + f"value_raw {value_raw!r} carries {len(numbers)} numbers; stage one " + "number per claim (a range is two claims)" + ) + return numbers[0] def parse_printed(value_raw: str) -> float: """The number a printed string carries, without its unit or scale. "£0.80bn" -> 0.8, "-800m" -> -800, "13,360" -> 13360, "(0.4)" -> -0.4, - "−£1.2 billion" -> -1.2, "2.1 per cent" -> 2.1. Exactly one number must + "£(0.4)bn" -> -0.4, "−£1.2 billion" -> -1.2, "2.1 per cent" -> 2.1, + "post-tax £0.8bn" -> 0.8. A minus sign counts only directly before the + number (a currency symbol may sit between them). Exactly one number must appear; a string with two ("£1.2bn to £1.5bn") is two claims (range_low, range_high), never one. """ - if not isinstance(value_raw, str) or not value_raw.strip(): - raise ValueError(f"value_raw must be a nonempty string: {value_raw!r}") + match = _number_match(value_raw) text = value_raw.strip() - numbers = _NUMBER.findall(text) - if len(numbers) != 1: - raise ValueError( - f"value_raw {value_raw!r} carries {len(numbers)} numbers; stage one " - "number per claim (a range is two claims)" - ) - magnitude = float(numbers[0].replace(",", "")) - before = text[: text.index(numbers[0])] - negative = any(m in before for m in _MINUS) or ( - text.startswith("(") and text.rstrip().endswith(")") - ) - if "minus" in before.lower(): + magnitude = float(match.group().replace(",", "")) + before, after = text[: match.start()], text[match.end() :] + negative = bool(_SIGN_BEFORE.search(before) or _MINUS_WORD_BEFORE.search(before)) + if _PAREN_BEFORE.search(before) and _PAREN_AFTER.match(after): negative = True return -magnitude if negative else magnitude +def printed_scale(value_raw: str) -> str | None: + """The scale a printed value states right after its number, if any.""" + match = _number_match(value_raw) + after = value_raw.strip()[match.end() :] + for pattern, scale in _SCALE_WORDS: + if pattern.match(after): + return scale + return None + + +def quote_contains(quote: str, value_raw: str) -> bool: + """Whether the quote prints this value as a whole number, sign included: + "0.07" is not in "10.07", and "800m" is not in "-800m".""" + q = " ".join(str(quote).split()) + raw = " ".join(str(value_raw).split()) + if not raw: + return False + lead = "" if raw[0] in _MINUS else _MINUS + pattern = rf"(? list[str]: - """Harvest rule 1 for one row: the value parses back from what was printed. + """Harvest rule 1 for one row: the value parses back from what was printed, + at the scale it states. A staged claim is never re-signed (its sign convention travels in conditions). A PolicyEngine estimate is stated in PolicyEngine's own @@ -195,8 +285,8 @@ def check_value(row: dict, where: str, signed: bool = False) -> list[str]: errors = [] sign = row.get("sign", 1) if signed: - if sign not in (1, -1): - return [f"{where}: sign must be 1 or -1"] + if type(sign) is not int or sign not in (1, -1): + return [f"{where}: sign must be the integer 1 or -1"] if sign == -1 and not _nonempty_str(row.get("sign_basis"), 10): errors.append(f"{where}: sign -1 needs sign_basis saying why") elif "sign" in row: @@ -209,6 +299,7 @@ def check_value(row: dict, where: str, signed: bool = False) -> list[str]: f"(scorecard_db/comparables.py NORMALIZATIONS: {sorted(NORMALIZATIONS)})" ) ] + factor, scale = NORMALIZATIONS[normalization] value = row.get("value") if not isinstance(value, (int, float)) or isinstance(value, bool): return [f"{where}: value must be a number, got {value!r}"] @@ -216,9 +307,15 @@ def check_value(row: dict, where: str, signed: bool = False) -> list[str]: return [f"{where}: value must be finite, got {value!r}"] try: parsed = parse_printed(row.get("value_raw")) + stated = printed_scale(row.get("value_raw")) except ValueError as exc: return [f"{where}: {exc}"] - expected = sign * parsed * NORMALIZATIONS[normalization] + if stated is not None and stated != scale: + errors.append( + f"{where}: value_raw {row.get('value_raw')!r} states the scale " + f"{stated!r}, but {normalization!r} assumes {scale or 'none'}" + ) + expected = sign * parsed * factor if not math.isclose(value, expected, rel_tol=1e-9, abs_tol=1e-9): errors.append( f"{where}: value {value!r} is not value_raw {row.get('value_raw')!r} " @@ -230,36 +327,68 @@ def check_value(row: dict, where: str, signed: bool = False) -> list[str]: # --- the gap ----------------------------------------------------------------- +# Words that turn a "positive = ..." phrase around, which signs.py's substring +# match would otherwise read as the plain phrase ("positive = yield forgone"). +_INVERTING_WORDS = ("forgone", "foregone", "lost") + def orientation(claim: dict, country: str) -> tuple[str, str]: """How the published value points relative to PolicyEngine's value for the metric: scorecard_db/signs.py decides it from the stated sign convention, - never from comparing the two signs.""" + never from comparing the two signs. + + For a change metric the convention must say what a positive value means + ("positive = ..."), match exactly one of signs.py's directions, and not + turn its phrase around. Anything else is unknown, so the claim cannot be + compared until the convention is stated plainly.""" + metric = claim.get("metric") + conditions = claim.get("conditions") or {} + text = (conditions.get("sign_convention") or "").strip().lower() + family = signs.ORIENTATION.get(metric) + if family is not None: + if not text.startswith("positive ="): + return signs.UNKNOWN, "convention must start 'positive = '" + unanswerable, opposite, same = signs._PHRASES[family] + hits_opposite = any(p in text for p in opposite) + hits_same = any(p in text for p in same) + if hits_opposite and hits_same: + return signs.UNKNOWN, "convention matches both directions" + if hits_same and any(w in text for w in _INVERTING_WORDS): + return signs.UNKNOWN, "convention turns its phrase around" return signs.classify( { - "metric": claim.get("metric"), - "conditions": claim.get("conditions") or {}, + "metric": metric, + "conditions": conditions, "country": country, "source": claim.get("source"), } ) +def oriented(external_value: float, relation: str) -> float | None: + """The published value in PolicyEngine's orientation.""" + if relation == signs.SAME: + return external_value + if relation == signs.OPPOSITE: + return -external_value + return None + + def relative_gap(pe_value: float, external_value: float, relation: str) -> float | None: """PolicyEngine / external - 1, with the external value in PolicyEngine's orientation. None when no rule orients the external value. A zero external value gives 0 when PolicyEngine is also zero and +inf otherwise, so a nonzero estimate against a published zero is always over - any threshold. Opposite signs give a gap below -1.""" - if relation == signs.SAME: - pe, ext = pe_value, external_value - elif relation == signs.OPPOSITE: - pe, ext = pe_value, -external_value - elif relation == signs.MAGNITUDE: + any threshold. Opposite signs give a gap below -1, which is over every + threshold the policy allows (thresholds are below 1).""" + if relation == signs.MAGNITUDE: pe, ext = abs(pe_value), abs(external_value) else: - return None + ext = oriented(external_value, relation) + if ext is None: + return None + pe = pe_value if ext == 0: return 0.0 if pe == 0 else math.inf return pe / ext - 1.0 @@ -269,15 +398,56 @@ def over_threshold(gap: float | None, threshold: float) -> bool: return gap is not None and abs(gap) > threshold +def sign_disagrees(pe_value: float, external_value: float | None) -> bool: + return ( + external_value is not None + and pe_value != 0 + and external_value != 0 + and (pe_value > 0) != (external_value > 0) + ) + + # --- loading ----------------------------------------------------------------- +def today_utc() -> str: + return dt.datetime.now(dt.timezone.utc).date().isoformat() + + +def _is_date(value) -> bool: + if not (isinstance(value, str) and _DATE.match(value)): + return False + try: + dt.date.fromisoformat(value) + except ValueError: + return False + return True + + +def _no_duplicate_keys(pairs): + out = {} + for key, value in pairs: + if key in out: + raise ValueError(f"duplicate key {key!r}") + out[key] = value + return out + + +def _loads(text: str): + return json.loads(text, object_pairs_hook=_no_duplicate_keys) + + def load_policy(path: Path = POLICY_PATH) -> dict: - policy = json.loads(path.read_text()) + policy = _loads(path.read_text()) threshold = policy.get("gap_threshold") - if not (isinstance(threshold, (int, float)) and 0 < threshold < 10): + # below 1, so a sign disagreement (a gap below -1) is always over it + if not ( + isinstance(threshold, (int, float)) + and not isinstance(threshold, bool) + and 0 < threshold < 1 + ): raise ValueError( - f"{path}: gap_threshold must be a positive number: {threshold!r}" + f"{path}: gap_threshold must be a number between 0 and 1: {threshold!r}" ) for key in ("promotion_window_days", "reharvest_window_days"): days = policy.get(key) @@ -286,20 +456,25 @@ def load_policy(path: Path = POLICY_PATH) -> dict: return policy -def load_producers(path: Path = PRODUCERS_PATH) -> dict: - data = json.loads(path.read_text()) +def load_producers(path: Path = PRODUCERS_PATH, today: str | None = None) -> dict: + data = _loads(path.read_text()) + today = today or today_utc() for country, producers in data["countries"].items(): slugs = [p["slug"] for p in producers] if len(slugs) != len(set(slugs)): raise ValueError(f"{path}: duplicate producer slugs for {country}") for p in producers: - applies = p["applies_to"] - if applies != "all" and not set(applies) <= set(data["scopes"][country]): + applies_to = p["applies_to"] + if applies_to != "all" and not set(applies_to) <= set( + data["scopes"][country] + ): + raise ValueError( + f"{path}: {p['slug']} applies_to unknown scope {applies_to}" + ) + if not _is_date(p["added"]) or p["added"] > today: raise ValueError( - f"{path}: {p['slug']} applies_to unknown scope {applies}" + f"{path}: {p['slug']} added must be a real date, no later than today" ) - if not _DATE.match(p["added"]): - raise ValueError(f"{path}: {p['slug']} added must be YYYY-MM-DD") return data @@ -308,6 +483,10 @@ def required_producers(producers: dict, country: str, on: str) -> list[dict]: return [p for p in producers["countries"][country] if p["added"] <= on] +def first_list_date(producers: dict, country: str) -> str: + return min(p["added"] for p in producers["countries"][country]) + + def applies(producer: dict, scope: str) -> bool: return producer["applies_to"] == "all" or scope in producer["applies_to"] @@ -330,8 +509,8 @@ def _read_jsonl(path: Path) -> list[dict]: if not line.strip(): continue try: - rows.append(json.loads(line)) - except json.JSONDecodeError as exc: + rows.append(_loads(line)) + except (ValueError, json.JSONDecodeError) as exc: raise ValueError(f"{path}:{n}: {exc}") from exc return rows @@ -370,9 +549,7 @@ def load_analysis(directory: Path) -> AnalysisRecord: continue try: parts[name] = ( - _read_jsonl(path) - if name.endswith(".jsonl") - else json.loads(path.read_text()) + _read_jsonl(path) if name.endswith(".jsonl") else _loads(path.read_text()) ) except (ValueError, json.JSONDecodeError) as exc: errors.append(f"{directory.name}: {name} does not parse: {exc}") @@ -393,6 +570,15 @@ def analysis_dirs(root: Path = SOURCES) -> list[Path]: ) +def pin(provenance: dict) -> str: + """One results file an estimate was read from: repo@commit:sha256.""" + return f"{provenance['repo']}@{provenance['commit']}:{provenance['sha256']}" + + +def current_pins(analysis: dict) -> list[str]: + return sorted({pin(e["provenance"]) for e in analysis.get("pe_estimates") or []}) + + # --- validation -------------------------------------------------------------- @@ -410,52 +596,160 @@ def _nonempty_str(value, minimum: int = 1) -> bool: return isinstance(value, str) and len(value.strip()) >= minimum +def _string_list(value) -> bool: + return isinstance(value, list) and all(isinstance(v, str) for v in value) + + def _baseline_labels() -> set[str]: from .baselines import BASELINES return {label for _, label, *_ in BASELINES} +def _repo_file(ref: str) -> bool: + path_part = ref.split("#")[0].strip() + if not path_part or path_part in (".", "/"): + return False + path = (REPO / path_part).resolve() + root = REPO.resolve() + return path.is_file() and root in path.parents + + def _check_basis(basis: dict, where: str) -> list[str]: + """A basis is a GitHub issue or pull request, engine metadata pinned to a + commit, or a document or diagnostic: a page URL or a file in this + repository. URLs and issues are checked for shape only; a reviewer opens + them.""" kind, ref = basis.get("kind"), basis.get("ref") if kind not in BASIS_KINDS: return [f"{where}: basis kind {kind!r} not in {BASIS_KINDS}"] - if not _nonempty_str(ref): - return [f"{where}: basis ref is empty"] + if not _nonempty_str(ref) or ref.strip() in ("#", ".", "/"): + return [f"{where}: basis ref is empty or a placeholder"] if kind == "issue": if not _GITHUB_ISSUE.match(ref): return [f"{where}: an issue basis is a GitHub issue or pull URL: {ref!r}"] return [] if kind == "engine_metadata": - if _GITHUB_PINNED.match(ref) or (REPO / ref.split("#")[0]).exists(): + if _GITHUB_PINNED.match(ref) or _repo_file(ref): return [] return [ ( f"{where}: engine metadata is a file pinned to a commit " - f"(github.com///blob/<40-hex>/) or a path in this repo: {ref!r}" + f"(github.com///blob/<40-hex>/) or a file in this repo: {ref!r}" ) ] - # document / diagnostic: a URL, or a path in this repository that exists - if _URL.match(ref): + if _URL_WITH_PATH.match(ref) or _repo_file(ref): return [] - if (REPO / ref.split("#")[0]).exists(): - return [] - return [f"{where}: {kind} basis {ref!r} is neither a URL nor a path in this repo"] + return [ + f"{where}: {kind} basis {ref!r} is neither an https page URL nor a file in this repo" + ] + + +def _check_bases(bases, where: str) -> list[str]: + if not (isinstance(bases, list) and bases): + return [f"{where}: basis must cite at least one source (no fabricated mechanisms)"] + errors = [] + for b in bases: + errors.extend(_check_basis(b if isinstance(b, dict) else {}, where)) + return errors + + +def _check_identity(row: dict, where: str) -> list[str]: + """The conditions that, with metric, unit, period, time basis and world, + say what quantity a figure is.""" + errors = [] + conditions = row.get("conditions") + if not _string_map(conditions): + return [f"{where}: conditions must map strings to strings"] + if conditions.get("basis") not in BASES: + errors.append(f"{where}: conditions.basis must be one of {BASES}") + if not _SLUG.match(conditions.get("program", "")): + errors.append(f"{where}: conditions.program must name the programme (a slug)") + fy = conditions.get("fy") + if fy is not None: + match = _FY.match(fy) + if not match or (int(match.group(1)) + 1) % 100 != int(match.group(2)): + errors.append(f"{where}: conditions.fy {fy!r} is not a financial year like 2027-28") + else: + end = int(match.group(1)) + 1 + if row.get("period") != end: + errors.append( + f"{where}: period {row.get('period')!r} is not {fy}'s end year {end}" + ) + if row.get("time_basis") != TimeBasis.FISCAL_YEAR.value: + errors.append(f"{where}: a figure for {fy} has time_basis fiscal_year") + return errors + + +def _check_estimate(e: dict, where: str, world_keys: set) -> list[str]: + errors = [] + if not _nonempty_str(e.get("label")): + errors.append(f"{where}: label is empty") + if e.get("world") not in world_keys: + errors.append(f"{where}: world {e.get('world')!r} is not defined") + if not _registered(Metric, e.get("metric")): + errors.append(f"{where}: metric {e.get('metric')!r} is not registered") + if not _registered(UnitConcept, e.get("unit_concept")): + errors.append(f"{where}: unit_concept {e.get('unit_concept')!r} is not registered") + if not _registered(TimeBasis, e.get("time_basis")): + errors.append(f"{where}: time_basis {e.get('time_basis')!r} is not registered") + if type(e.get("period")) is not int: + errors.append(f"{where}: period must be an integer year") + errors.extend(_check_identity(e, where)) + errors.extend(check_value(e, where, signed=True)) + prov = e.get("provenance") or {} + if not (isinstance(prov.get("repo"), str) and _REPO.match(prov["repo"])): + errors.append(f"{where}: provenance.repo must be /") + if not (isinstance(prov.get("commit"), str) and _COMMIT.match(prov["commit"])): + errors.append(f"{where}: provenance.commit must be a 40-hex commit") + if not (isinstance(prov.get("sha256"), str) and _SHA256.match(prov["sha256"])): + errors.append(f"{where}: provenance.sha256 must be the results file's SHA-256") + for f in ("path", "field", "dataset"): + if not _nonempty_str(prov.get(f)): + errors.append(f"{where}: provenance.{f} is empty") + if not (isinstance(prov.get("computed_at"), str) and _DATETIME.match(prov["computed_at"])): + errors.append(f"{where}: provenance.computed_at must be an ISO date or time") + engine = prov.get("engine") + if not (_string_map(engine) and engine): + errors.append(f"{where}: provenance.engine must map package names to versions") + return errors + + +def _same_quantity(claim: dict, estimate: dict) -> bool: + return all(claim.get(f) == estimate.get(f) for f in IDENTITY_FIELDS) and all( + (claim.get("conditions") or {}).get(f) == (estimate.get("conditions") or {}).get(f) + for f in IDENTITY_CONDITIONS + ) + + +def _near(claim: dict, estimate: dict) -> bool: + """An estimate is near a claim when it is for the same period and, where the + claim names them, the same metric and unit: a claim on a published + quantity is either compared or set beside it with a stated mismatch.""" + if claim.get("period") != estimate.get("period"): + return False + for f in ("metric", "unit_concept"): + if claim.get(f) is not None and claim.get(f) != estimate.get(f): + return False + return True def validate( record: AnalysisRecord, producers: dict | None = None, policy: dict | None = None, + today: str | None = None, ) -> list[str]: """Every reason this directory is not a complete comparables record.""" producers = producers or load_producers() policy = policy or load_policy() + today = today or today_utc() errors = list(record.load_errors) if errors: return errors name = record.name a = record.analysis + labels = _baseline_labels() # analysis.json ----------------------------------------------------------- if a.get("schema") != ANALYSIS_SCHEMA: @@ -495,10 +789,9 @@ def validate( ): errors.append(f"{name}: owner.author must be the app-v2 authors.json key") published = a.get("published") - if published is not None and not ( - isinstance(published, str) and _DATE.match(published) - ): - errors.append(f"{name}: published must be YYYY-MM-DD or null") + if published is not None and not (_is_date(published) and published <= today): + errors.append(f"{name}: published must be a real date no later than today, or null") + published = None worlds = a.get("worlds") if not isinstance(worlds, dict): errors.append(f"{name}: worlds must map a world key to its policy slug") @@ -508,67 +801,40 @@ def validate( errors.append(f"{name}: world key {wkey!r} is reserved or malformed") if not (isinstance(world, dict) and _SLUG.match(str(world.get("policy", "")))): errors.append(f"{name}: world {wkey!r} needs a policy slug") - if not _nonempty_str((world or {}).get("description"), 10): + continue + if not _nonempty_str(world.get("description"), 10): errors.append(f"{name}: world {wkey!r} needs a description") + if world.get("baseline") is not None and world["baseline"] not in labels: + errors.append( + f"{name}: world {wkey!r} baseline {world['baseline']!r} is not " + "registered in scorecard_db/baselines.py" + ) world_keys = set(worlds) | {BASELINE_WORLD} estimates: dict[str, dict] = {} for i, e in enumerate(a.get("pe_estimates") or []): - where = f"{name}: pe_estimates[{i}]" key = e.get("key") if not (isinstance(key, str) and _KEY.match(key)) or key in estimates: - errors.append(f"{where}: key {key!r} missing, malformed or repeated") + errors.append(f"{name}: pe_estimates[{i}]: key {key!r} missing, malformed or repeated") continue estimates[key] = e - where = f"{name}: estimate {key}" - if not _nonempty_str(e.get("label")): - errors.append(f"{where}: label is empty") - if e.get("world") not in world_keys: - errors.append(f"{where}: world {e.get('world')!r} is not defined") - if not _registered(Metric, e.get("metric")): - errors.append(f"{where}: metric {e.get('metric')!r} is not registered") - if not _registered(UnitConcept, e.get("unit_concept")): - errors.append( - f"{where}: unit_concept {e.get('unit_concept')!r} is not registered" - ) - if not _registered(TimeBasis, e.get("time_basis")): - errors.append( - f"{where}: time_basis {e.get('time_basis')!r} is not registered" - ) - if not isinstance(e.get("period"), int) or isinstance(e.get("period"), bool): - errors.append(f"{where}: period must be an integer year") - if not _string_map(e.get("conditions", {})): - errors.append(f"{where}: conditions must map strings to strings") - errors.extend(check_value(e, where, signed=True)) - prov = e.get("provenance") or {} - if not (isinstance(prov.get("repo"), str) and _REPO.match(prov["repo"])): - errors.append(f"{where}: provenance.repo must be /") - if not (isinstance(prov.get("commit"), str) and _COMMIT.match(prov["commit"])): - errors.append(f"{where}: provenance.commit must be a 40-hex commit") - if not (isinstance(prov.get("sha256"), str) and _SHA256.match(prov["sha256"])): - errors.append( - f"{where}: provenance.sha256 must be the results file's SHA-256" - ) - for f in ("path", "field", "dataset"): - if not _nonempty_str(prov.get(f)): - errors.append(f"{where}: provenance.{f} is empty") - if not ( - isinstance(prov.get("computed_at"), str) - and _DATETIME.match(prov["computed_at"]) - ): - errors.append( - f"{where}: provenance.computed_at must be an ISO date or time" - ) - engine = prov.get("engine") - if not (_string_map(engine) and engine): - errors.append( - f"{where}: provenance.engine must map package names to versions" - ) + errors.extend(_check_estimate(e, f"{name}: estimate {key}", world_keys)) if not estimates: errors.append( f"{name}: pe_estimates is empty; an analysis that publishes no estimate " "is declared no_estimate in the registry, not recorded here" ) + superseded_pins = set() + for i, e in enumerate(a.get("superseded_estimates") or []): + where = f"{name}: superseded_estimates[{i}]" + if e.get("key") not in estimates: + errors.append(f"{where}: key {e.get('key')!r} is not a current estimate's") + if not (_is_date(e.get("superseded_on")) and e["superseded_on"] <= today): + errors.append(f"{where}: superseded_on must be a real date no later than today") + errors.extend(_check_estimate(e, where, world_keys)) + if not any(err.startswith(where) and "provenance" in err for err in errors): + superseded_pins.add(pin(e["provenance"])) + pins_now = set(current_pins(a)) if not any("provenance" in err for err in errors) else set() # manifest ---------------------------------------------------------------- manifest: dict[str, dict] = {} @@ -594,26 +860,61 @@ def validate( if not searches: errors.append(f"{name}: search_log has no searches") claim_found: dict[str, tuple[str, str, str]] = {} # key -> (date, kind, producer) + earliest = first_list_date(producers, country) + first_date = None previous_date = "" for i, s in enumerate(searches): where = f"{name}: search {i + 1}" date, skind = s.get("date"), s.get("kind") - if not (isinstance(date, str) and _DATE.match(date)): - errors.append(f"{where}: date must be YYYY-MM-DD") + if not _is_date(date): + errors.append(f"{where}: date must be a real YYYY-MM-DD date") continue + if date > today: + errors.append(f"{where}: dated {date}, after today ({today})") + if date < earliest: + errors.append(f"{where}: dated {date}, before the {country} producer list ({earliest})") if date < previous_date: errors.append(f"{where}: searches must be in date order") previous_date = date if skind not in SEARCH_KINDS: errors.append(f"{where}: kind {skind!r} not in {SEARCH_KINDS}") - if i == 0 and skind in POST_PUBLICATION_KINDS: + if i == 0: + first_date = date + if skind not in FIRST_KINDS: + errors.append(f"{where}: the first search is a prepublish or backfill search") + elif skind in FIRST_KINDS: + errors.append(f"{where}: only the first search is {skind}") + if skind == "prepublish" and published is not None and date > published: errors.append( - f"{where}: the first search is a prepublish or backfill search" + f"{where}: a prepublish search is dated on or before publication ({published})" ) + if skind == "backfill": + if published is None: + errors.append(f"{where}: a backfill search needs analysis.json published") + elif date < published: + errors.append(f"{where}: a backfill search is dated after publication ({published})") + if skind in LATER_KINDS: + if published is None: + errors.append(f"{where}: a {skind} search needs analysis.json published") + elif date <= published: + errors.append(f"{where}: a {skind} search is dated after publication ({published})") + if first_date is not None and date <= first_date: + errors.append(f"{where}: a {skind} search is dated after the first search ({first_date})") if not _nonempty_str(s.get("by")): errors.append(f"{where}: by must name the session or Subfleet job") - entries = s.get("producers") or [] - logged = [p.get("producer") for p in entries] + pins = s.get("results_pins") + if not (_string_list(pins) and pins and all(_PIN.match(p) for p in pins)): + errors.append( + f"{where}: results_pins must list the results files compared " + "(repo@commit:sha256; `comparables pins` prints them)" + ) + elif pins_now and not set(pins) <= pins_now | superseded_pins: + errors.append(f"{where}: results_pins name a results file no estimate is read from") + entries = s.get("producers") + if not isinstance(entries, list): + errors.append(f"{where}: producers must be a list") + entries = [] + logged = [p.get("producer") for p in entries if isinstance(p, dict)] required = required_producers(producers, country, date) required_slugs = [p["slug"] for p in required] missing = [p for p in required_slugs if p not in logged] @@ -621,25 +922,29 @@ def validate( if missing: errors.append(f"{where}: producers not searched: {missing}") if unknown: - errors.append( - f"{where}: producers not on the {country} list on {date}: {unknown}" - ) + errors.append(f"{where}: producers not on the {country} list on {date}: {unknown}") if len(logged) != len(set(logged)): errors.append(f"{where}: a producer is logged twice") by_slug = {p["slug"]: p for p in required} for entry in entries: + if not isinstance(entry, dict): + errors.append(f"{where}: each producer entry is an object") + continue slug_ = entry.get("producer") pwhere = f"{where} {slug_}" result = entry.get("result") - claims = entry.get("claims") or [] - queries = entry.get("queries") or [] - checked = entry.get("checked") or [] + claims = entry.get("claims", []) + queries = entry.get("queries", []) + checked = entry.get("checked", []) + if not (_string_list(claims) and _string_list(queries) and _string_list(checked)): + errors.append(f"{pwhere}: claims, queries and checked are lists of strings") + continue if result not in RESULTS: errors.append(f"{pwhere}: result {result!r} not in {RESULTS}") continue if not all(_nonempty_str(q) for q in queries): - errors.append(f"{pwhere}: queries must be strings") - if not all(isinstance(u, str) and _URL.match(u) for u in checked): + errors.append(f"{pwhere}: queries must be nonempty strings") + if not all(_URL.match(u) for u in checked): errors.append(f"{pwhere}: checked must be URLs") producer = by_slug.get(slug_) if result == "not_applicable": @@ -652,18 +957,14 @@ def validate( errors.append(f"{pwhere}: not_applicable with claims") continue if not (queries or checked): - errors.append( - f"{pwhere}: {result} with no query run and no page checked" - ) + errors.append(f"{pwhere}: {result} with no query run and no page checked") if result == "none_found" and claims: errors.append(f"{pwhere}: none_found with claims") if result == "found_not_staged": if claims: errors.append(f"{pwhere}: found_not_staged with claims") if not checked: - errors.append( - f"{pwhere}: found_not_staged lists the pages it was seen on" - ) + errors.append(f"{pwhere}: found_not_staged lists the pages it was seen on") if not _nonempty_str(entry.get("note"), 20): errors.append( f"{pwhere}: found_not_staged says what was found and why it is not staged" @@ -672,15 +973,22 @@ def validate( errors.append(f"{pwhere}: found with no claims") for key in claims: if key in claim_found: - errors.append( - f"{pwhere}: claim {key} already found by an earlier search" - ) + errors.append(f"{pwhere}: claim {key} already found by an earlier search") claim_found[key] = (date, skind, slug_) + if searches and pins_now: + latest = searches[-1].get("results_pins") + if _string_list(latest) and set(latest) != pins_now: + errors.append( + f"{name}: the latest search compared {sorted(latest)}, but the estimates " + f"are now read from {sorted(pins_now)}; a new results file needs a new search" + ) # claims ---------------------------------------------------------------------- - labels = _baseline_labels() claims_by_key: dict[str, dict] = {} must_annotate: list[str] = [] + compared_keys: list[str] = [] + used_primaries: set[str] = set() + range_groups: dict[str, list[dict]] = defaultdict(list) for i, c in enumerate(record.claims): key = c.get("key") where = f"{name}: claim {key or i + 1}" @@ -693,93 +1001,71 @@ def validate( errors.append(f"{where}: no search entry lists this claim") else: if c.get("found_on") != found[0]: - errors.append( - f"{where}: found_on must be {found[0]}, the search that found it" - ) + errors.append(f"{where}: found_on must be {found[0]}, the search that found it") if c.get("found_by") != found[2]: - errors.append( - f"{where}: found_by must be {found[2]}, the producer logged" - ) + errors.append(f"{where}: found_by must be {found[2]}, the producer logged") source = c.get("source") found_by = c.get("found_by") if not (isinstance(source, str) and _SLUG.match(source)): errors.append(f"{where}: source must be a producer slug") - elif source == "open_search": + elif source in BANNED_SOURCES: errors.append( - f"{where}: source names the producer that published it, not open_search" + f"{where}: source names the producer that published the figure " + f"(not {source!r})" ) - elif found_by not in OPEN_FINDERS: - # a listed producer's own search finds only that producer's claims + else: belongs = producer_for_source(producers, country, source) - if belongs != found_by: + if belongs is not None and belongs != found_by: errors.append( - f"{where}: source {source!r} is not {found_by}'s; a claim from " - "another producer is logged under that producer, or under " - "open_search when the producer is not on the list" + f"{where}: {source!r} is on the list as {belongs}; its claims are " + f"logged under {belongs}'s own entry, not {found_by}" + ) + if belongs is None and found_by not in OPEN_FINDERS: + errors.append( + f"{where}: {source!r} is not on the {country} list; log it under " + "open_search or academic" ) metric = c.get("metric") if metric is None: if not _nonempty_str(c.get("proposed_metric")): errors.append(f"{where}: metric null needs proposed_metric") elif not _registered(Metric, metric): - errors.append( - f"{where}: metric {metric!r} is not registered; use proposed_metric" - ) + errors.append(f"{where}: metric {metric!r} is not registered; use proposed_metric") unit = c.get("unit_concept") if unit is None: if not _nonempty_str(c.get("proposed_unit")): errors.append(f"{where}: unit_concept null needs proposed_unit") elif not _registered(UnitConcept, unit): - errors.append( - f"{where}: unit_concept {unit!r} is not registered; use proposed_unit" - ) - if c.get("value_kind") not in VALUE_KINDS: - errors.append( - f"{where}: value_kind {c.get('value_kind')!r} not in {VALUE_KINDS}" - ) - if c.get("value_kind") == "approx_chart_reading": + errors.append(f"{where}: unit_concept {unit!r} is not registered; use proposed_unit") + value_kind = c.get("value_kind") + if value_kind not in VALUE_KINDS: + errors.append(f"{where}: value_kind {value_kind!r} not in {VALUE_KINDS}") + errors.extend(check_value(c, where)) + if value_kind == "approx_chart_reading": if not _nonempty_str(c.get("note"), 10): errors.append(f"{where}: a chart reading says how it was read in note") - else: - errors.extend(check_value(c, where)) - quote = re.sub(r"[\s,]", "", str(c.get("quote", ""))) - raw_digits = "".join(_NUMBER.findall(str(c.get("value_raw", "")))).replace( - ",", "" + elif not quote_contains(c.get("quote", ""), c.get("value_raw", "")): + errors.append( + f"{where}: quote does not print the value {c.get('value_raw')!r} " + "(as a whole number, sign included)" ) - if raw_digits and raw_digits not in quote: - errors.append( - f"{where}: quote does not contain the printed value {c.get('value_raw')!r}" - ) - if not isinstance(c.get("period"), int) or isinstance(c.get("period"), bool): + if type(c.get("period")) is not int: errors.append(f"{where}: period must be an integer year (FY end year)") if not _registered(TimeBasis, c.get("time_basis")): - errors.append( - f"{where}: time_basis {c.get('time_basis')!r} is not registered" - ) - if not _string_map(c.get("conditions")): - errors.append(f"{where}: conditions must map strings to strings") + errors.append(f"{where}: time_basis {c.get('time_basis')!r} is not registered") + errors.extend(_check_identity(c, where)) world = c.get("world") if world not in world_keys: errors.append(f"{where}: world {world!r} is not defined in analysis.json") if world != BASELINE_WORLD and not _nonempty_str(c.get("reform_hint")): - errors.append( - f"{where}: reform_hint must quote the producer's own words for the world" - ) + errors.append(f"{where}: reform_hint must quote the producer's own words for the world") if c.get("attribution") not in ATTRIBUTIONS: - errors.append( - f"{where}: attribution {c.get('attribution')!r} not in {ATTRIBUTIONS}" - ) + errors.append(f"{where}: attribution {c.get('attribution')!r} not in {ATTRIBUTIONS}") bclass = c.get("benchmark_class") if bclass not in BENCHMARK_CLASSES: - errors.append( - f"{where}: benchmark_class {bclass!r} not in {BENCHMARK_CLASSES}" - ) - if (c.get("attribution") == "same_assumptions") != ( - bclass == "same_assumptions" - ): - errors.append( - f"{where}: same_assumptions attribution and benchmark_class go together" - ) + errors.append(f"{where}: benchmark_class {bclass!r} not in {BENCHMARK_CLASSES}") + if (c.get("attribution") == "same_assumptions") != (bclass == "same_assumptions"): + errors.append(f"{where}: same_assumptions attribution and benchmark_class go together") if bclass == "administrative_fact" and world == BASELINE_WORLD: errors.append( f"{where}: an administrative level is calibration material; stage it " @@ -792,20 +1078,22 @@ def validate( "scorecard_db/baselines.py (harvest rule 7: register it first)" ) if "proposed_baseline" in c: - errors.append( - f"{where}: register the baseline before staging (harvest rule 7)" - ) + errors.append(f"{where}: register the baseline before staging (harvest rule 7)") pub = c.get("publication") or {} - for f in ("title", "url", "date", "publisher", "locator", "access"): + for f in ("title", "url", "publisher", "locator", "access"): if not _nonempty_str(pub.get(f)): errors.append(f"{where}: publication.{f} is empty") + if not _is_date(pub.get("date")): + errors.append(f"{where}: publication.date must be a real YYYY-MM-DD date") + elif _is_date(c.get("found_on")) and pub["date"] > c["found_on"]: + errors.append(f"{where}: published {pub['date']}, after it was found ({c['found_on']})") sha = pub.get("primary_sha256") if sha not in manifest: - errors.append( - f"{where}: publication.primary_sha256 is not in manifest.jsonl" - ) - elif manifest[sha].get("url") != pub.get("url"): - errors.append(f"{where}: publication.url differs from its manifest line") + errors.append(f"{where}: publication.primary_sha256 is not in manifest.jsonl") + else: + used_primaries.add(sha) + if manifest[sha].get("url") != pub.get("url"): + errors.append(f"{where}: publication.url differs from its manifest line") for f in ("source_table", "source_column", "quote"): if not _nonempty_str(c.get(f)): errors.append(f"{where}: {f} is empty") @@ -813,44 +1101,59 @@ def validate( errors.append(f"{where}: parse_confidence not in {PARSE_CONFIDENCE}") if c.get("status") != "ok": errors.append(f"{where}: status must be 'ok'") + if value_kind in RANGE_KINDS: + group = (c.get("conditions") or {}).get("range_group") + if not (isinstance(group, str) and _SLUG.match(group)): + errors.append(f"{where}: a range bound names its range_group in conditions") + else: + range_groups[group].append(c) compare = c.get("compare_to") + nearest = c.get("nearest_estimate") if compare is None: if not _nonempty_str(c.get("pe_missing"), 10): errors.append(f"{where}: not compared, so pe_missing must say why") - # A claim on the same quantity as a published estimate cannot be - # waved off with a sentence: it names that estimate, and an - # annotation with a basis says why the two are not like for like. - matching = sorted( - e["key"] - for e in estimates.values() - if all(c.get(f) == e.get(f) for f in MATCH_FIELDS) - ) - nearest = c.get("nearest_estimate") - if matching and nearest not in matching: - errors.append( - f"{where}: matches {', '.join(matching)} on metric, unit, period " - "and time basis; compare it (compare_to), or name it in " - "nearest_estimate and annotate why they differ in concept" - ) - elif not matching and nearest is not None: + near = sorted(k for k, e in estimates.items() if _near(c, e)) + if near and nearest not in near: errors.append( - f"{where}: nearest_estimate {nearest!r} does not match this claim" + f"{where}: estimates {', '.join(near)} are for the same period" + + (" and metric" if metric else "") + + "; compare it (compare_to), or name the nearest in " + "nearest_estimate and annotate why they differ" ) - elif matching: + elif not near and nearest is not None: + errors.append(f"{where}: nearest_estimate {nearest!r} is not near this claim") + elif near: must_annotate.append(key) + est = estimates[nearest] + if ( + _same_quantity(c, est) + and c.get("attribution") == "own" + and value_kind not in ("cumulative",) + and baseline == worlds.get(est.get("world"), {}).get("baseline") + ): + errors.append( + f"{where}: it is the same quantity as {nearest} (metric, unit, " + "period, time basis, world, basis and programme); compare it" + ) continue + if nearest is not None: + errors.append(f"{where}: a compared claim has no nearest_estimate") if c.get("pe_missing") is not None: errors.append(f"{where}: compared, so pe_missing must be absent") - est = estimates.get((compare or {}).get("pe_estimate")) + if not isinstance(compare, dict): + errors.append(f"{where}: compare_to must be an object naming pe_estimate") + continue + est = estimates.get(compare.get("pe_estimate")) if est is None: errors.append(f"{where}: compare_to names no PolicyEngine estimate") continue + compared_keys.append(key) if c.get("attribution") != "own": - errors.append( - f"{where}: only a producer's own estimate is compared ({c.get('attribution')})" - ) + errors.append(f"{where}: only a producer's own estimate is compared ({c.get('attribution')})") + if value_kind == "cumulative": + errors.append(f"{where}: a cumulative figure is not compared with a single-year estimate") if c.get("world") != est.get("world"): - across = (compare or {}).get("across_worlds") + across = compare.get("across_worlds") if not isinstance(across, dict): errors.append( f"{where}: world {c.get('world')!r} differs from {est['key']}'s " @@ -859,33 +1162,56 @@ def validate( ) else: if not _nonempty_str(across.get("reason"), 20): - errors.append( - f"{where}: across_worlds.reason must say why the quantities match" - ) - bases = across.get("basis") - if not (isinstance(bases, list) and bases): - errors.append(f"{where}: across_worlds.basis must cite a source") - else: - for b in bases: - errors.extend( - _check_basis(b if isinstance(b, dict) else {}, where) - ) + errors.append(f"{where}: across_worlds.reason must say why the quantities match") + errors.extend(_check_bases(across.get("basis"), where)) for f in ("metric", "unit_concept", "period", "time_basis"): if c.get(f) != est.get(f): errors.append( f"{where}: {f} {c.get(f)!r} differs from {est['key']}'s {est.get(f)!r}; " - "a different quantity is not compared (pe_missing says so)" + "a different quantity is not compared" ) - relation, basis = orientation(c, country) - if relation == signs.UNKNOWN: + for f in IDENTITY_CONDITIONS: + mine = (c.get("conditions") or {}).get(f) + theirs = (est.get("conditions") or {}).get(f) + if mine != theirs: + errors.append( + f"{where}: {f} {mine!r} differs from {est['key']}'s {theirs!r}; " + "a different quantity is not compared" + ) + executed = worlds.get(est.get("world"), {}).get("baseline") + if baseline != executed: + errors.append( + f"{where}: scored against baseline {baseline or 'current law'}, but " + f"{est['key']} against {executed or 'current law'}" + ) + relation, why = orientation(c, country) + if relation not in (signs.SAME, signs.OPPOSITE) and metric in signs.ORIENTATION: errors.append( - f"{where}: the sign convention does not orient the value ({basis}); " - "state it in conditions.sign_convention in words signs.py reads" + f"{where}: the sign convention does not orient the value ({why}); " + "state what a positive value means in conditions.sign_convention, " + "in words signs.py reads" ) + elif relation == signs.UNKNOWN: + errors.append(f"{where}: the value cannot be oriented ({why})") for key in claim_found: if key not in claims_by_key: errors.append(f"{name}: search lists claim {key}, which is not staged") + for sha, m in manifest.items(): + if sha not in used_primaries: + errors.append(f"{name}: manifest line for {m.get('url')} is cited by no claim") + for group, members in range_groups.items(): + kinds = sorted(m.get("value_kind") for m in members) + if kinds != ["range_high", "range_low"]: + errors.append(f"{name}: range_group {group} needs exactly one range_low and one range_high") + continue + low, high = members + shared = ("source", "metric", "unit_concept", "period", "time_basis", "world", "compare_to") + if any(low.get(f) != high.get(f) for f in shared) or any( + (low.get("conditions") or {}).get(f) != (high.get("conditions") or {}).get(f) + for f in IDENTITY_CONDITIONS + ): + errors.append(f"{name}: range_group {group}'s bounds measure different things") # annotations ------------------------------------------------------------------- annotated: dict[str, dict] = {} @@ -902,28 +1228,14 @@ def validate( errors.append(f"{where}: status {status!r} not in {ANNOTATION_STATUSES}") if not _nonempty_str(n.get("cause"), 20): errors.append(f"{where}: cause must state the cause (20+ characters)") - bases = n.get("basis") - if not (isinstance(bases, list) and bases): - errors.append( - f"{where}: basis must cite at least one source (no fabricated mechanisms)" - ) - else: - for b in bases: - errors.extend(_check_basis(b if isinstance(b, dict) else {}, where)) + errors.extend(_check_bases(n.get("basis"), where)) if not isinstance(n.get("explains_gap"), bool): errors.append(f"{where}: explains_gap must be true or false") if status == "pending": if n.get("explains_gap"): - errors.append( - f"{where}: a pending annotation does not explain the gap yet" - ) - if not ( - isinstance(n.get("tracking_issue"), str) - and _GITHUB_ISSUE.match(n["tracking_issue"]) - ): - errors.append( - f"{where}: pending needs the tracking_issue opened for the owner" - ) + errors.append(f"{where}: a pending annotation does not explain the gap yet") + if not (isinstance(n.get("tracking_issue"), str) and _GITHUB_ISSUE.match(n["tracking_issue"])): + errors.append(f"{where}: pending needs the tracking_issue opened for the owner") keys = n.get("claims") if not (isinstance(keys, list) and keys): errors.append(f"{where}: claims must list the claim keys it covers") @@ -931,26 +1243,38 @@ def validate( for key in keys: if key not in claims_by_key: errors.append(f"{where}: claim {key} is not staged") - elif key in annotated: - errors.append( - f"{where}: claim {key} already has a status from {annotated[key]['id']}" - ) - else: - annotated[key] = n - if ( - status == "pending" - and claim_found.get(key, ("", ""))[1] not in POST_PUBLICATION_KINDS - ): + continue + if key in annotated: + errors.append(f"{where}: claim {key} already has a status from {annotated[key]['id']}") + continue + annotated[key] = n + c = claims_by_key[key] + if key in compared_keys: + if status not in COMPARED_STATUSES: + errors.append( + f"{where}: {key} is compared, so its status is one of " + f"{COMPARED_STATUSES}; a concept mismatch is not compared" + ) + est = estimates[c["compare_to"]["pe_estimate"]] + if status == ComparisonStatus.COMPARABLE.value and c.get("world") != est.get("world"): errors.append( - f"{where}: {key} was found before publication; its gap is " - "explained before the analysis is recorded, not left pending" + f"{where}: {key} is compared across worlds, so it is constructed, " + "not comparable" ) + elif status not in MISMATCH_STATUSES: + errors.append( + f"{where}: {key} is not compared, so its status is one of {MISMATCH_STATUSES}" + ) + if status == "pending" and claim_found.get(key, ("", ""))[1] not in POST_PUBLICATION_KINDS: + errors.append( + f"{where}: {key} was found before publication; its gap is " + "explained before the analysis is listed, not left pending" + ) for key in must_annotate: - note = annotated.get(key) - if note is None or note.get("status") not in MISMATCH_STATUSES: + if key not in annotated: errors.append( - f"{name}: claim {key} is not compared with its nearest estimate, so an " + f"{name}: claim {key} sits beside its nearest estimate uncompared, so an " f"annotation with status {' or '.join(MISMATCH_STATUSES)} must say why" ) @@ -961,9 +1285,12 @@ def validate( if not row["over"]: continue note = annotated.get(row["key"]) + gap = row["group_gap"] if row["range_group"] else row["gap"] + shown = "unbounded" if gap is None or math.isinf(gap) else f"{gap:+.1%}" if note is None: errors.append( - f"{name}: claim {row['key']} is {row['gap']:+.1%} from {row['estimate']} " + f"{name}: claim {row['key']} is {shown} from {row['estimate']}" + f"{' (its sign disagrees)' if row['sign_disagrees'] else ''} " f"(threshold {policy['gap_threshold']:.0%}) and no annotation explains it" ) elif note["status"] != "pending" and not note.get("explains_gap"): @@ -975,8 +1302,8 @@ def validate( def evaluate(record: AnalysisRecord, policy: dict | None = None) -> list[dict]: - """One row per staged claim: the comparison and its gap. Assumes a valid - record (``validate`` returns no errors).""" + """One row per staged claim: the comparison and its gap, or its nearest + estimate and that gap for information. Assumes a valid record.""" policy = policy or load_policy() threshold = policy["gap_threshold"] country = record.analysis["country"] @@ -996,17 +1323,24 @@ def evaluate(record: AnalysisRecord, policy: dict | None = None) -> list[dict]: "nearest": c.get("nearest_estimate"), "relation": None, "gap": None, + "nearest_gap": None, "over": False, + "sign_disagrees": False, + "range_group": (c.get("conditions") or {}).get("range_group") + if c.get("value_kind") in RANGE_KINDS + else None, + "group_gap": None, "status": note["status"] if note else None, "annotation": note["id"] if note else None, "explains_gap": bool(note and note.get("explains_gap")), "tracking_issue": (note or {}).get("tracking_issue"), } + relation, _ = orientation(c, country) compare = c.get("compare_to") if compare: est = estimates[compare["pe_estimate"]] - relation, _ = orientation(c, country) gap = relative_gap(est["value"], c["value"], relation) + disagrees = sign_disagrees(est["value"], oriented(c["value"], relation)) # a comparison across two policy worlds is a documented # construction, never plainly comparable default = ( @@ -1018,10 +1352,40 @@ def evaluate(record: AnalysisRecord, policy: dict | None = None) -> list[dict]: estimate=est["key"], relation=relation, gap=gap, - over=over_threshold(gap, threshold), + sign_disagrees=disagrees, + over=over_threshold(gap, threshold) or disagrees, status=row["status"] or default, ) + elif row["nearest"] and c.get("metric") is not None: + row["relation"] = relation + row["nearest_gap"] = relative_gap( + estimates[row["nearest"]]["value"], c["value"], relation + ) rows.append(row) + + # A range is over only when the estimate lies outside it by more than the + # threshold from the nearer bound, or on the other side of zero. + by_group: dict[str, list[dict]] = defaultdict(list) + for row, c in zip(rows, record.claims): + if row["range_group"] and row["estimate"]: + by_group[row["range_group"]].append((row, c)) + for members in by_group.values(): + if len(members) != 2: + continue + pe = estimates[members[0][0]["estimate"]]["value"] + bounds = [oriented(c["value"], r["relation"]) for r, c in members] + if None in bounds: + continue + lo, hi = min(bounds), max(bounds) + if lo <= pe <= hi: + group_gap, over = 0.0, False + else: + nearer = lo if pe < lo else hi + group_gap = (pe / nearer - 1.0) if nearer != 0 else math.inf + over = over_threshold(group_gap, threshold) or sign_disagrees(pe, nearer) + for r, _ in members: + r["group_gap"] = group_gap + r["over"] = over return rows @@ -1038,6 +1402,7 @@ def _search_summary(search: dict) -> dict: return { "date": search["date"], "kind": search["kind"], + "results_pins": sorted(search["results_pins"]), "producers_searched": searched, "producers_with_finds": sorted(by_result["found"]), "found_not_staged": sorted(by_result["found_not_staged"]), @@ -1047,9 +1412,7 @@ def _search_summary(search: dict) -> dict: def _round(x: float | None) -> float | None: - if x is None: - return None - if math.isinf(x): + if x is None or math.isinf(x): return None return round(x, 4) @@ -1069,19 +1432,18 @@ def index_entry(record: AnalysisRecord, policy: dict) -> dict: "url": a["url"], "owner": a["owner"]["author"], "published": a.get("published"), + "current_pins": current_pins(a), "searches": [_search_summary(s) for s in record.search_log["searches"]], "claims": len(rows), "compared": len(compared), + "beside_nearest": sum(1 for r in rows if r["nearest"]), "over_threshold": len(over), + "sign_disagreements": sum(1 for r in compared if r["sign_disagrees"]), "explained": sum(1 for r in over if r["explains_gap"]), "pending": sum(1 for r in rows if r["status"] == "pending"), "unbounded_gaps": sum(1 for g in gaps if math.isinf(g)), - "max_abs_gap": _round( - max((g for g in gaps if not math.isinf(g)), default=None) - ), - "tracking_issues": sorted( - {r["tracking_issue"] for r in rows if r["tracking_issue"]} - ), + "max_abs_gap": _round(max((g for g in gaps if not math.isinf(g)), default=None)), + "tracking_issues": sorted({r["tracking_issue"] for r in rows if r["tracking_issue"]}), "search_log": f"{REPO_URL}/blob/main/sources/{record.name}/search_log.json", "comparisons": _comparisons(record, rows), } @@ -1089,8 +1451,8 @@ def index_entry(record: AnalysisRecord, policy: dict) -> dict: def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: """Every staged estimate as a reader would see it: the producer's figure - as printed, ours where one compares, the gap and why. The data a "How we - compare" section on the analysis's page reads.""" + as printed, ours where one compares (or the nearest one), the gap and + why. The data a "How we compare" section on the analysis's page reads.""" claims = {c["key"]: c for c in record.claims} estimates = {e["key"]: e for e in record.analysis["pe_estimates"]} causes = {n["id"]: n["cause"] for n in record.annotations} @@ -1098,6 +1460,7 @@ def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: for row in rows: c = claims[row["key"]] est = estimates.get(row["estimate"]) if row["estimate"] else None + conditions = c.get("conditions") or {} out.append( { "claim": c["key"], @@ -1108,19 +1471,28 @@ def _comparisons(record: AnalysisRecord, rows: list[dict]) -> list[dict]: "url": c["publication"]["url"], "locator": c["publication"]["locator"], "metric": c.get("metric") or c.get("proposed_metric"), + "basis": conditions.get("basis"), + "program": conditions.get("program"), "period": c["period"], - "fy": (c.get("conditions") or {}).get("fy"), + "fy": conditions.get("fy"), + "value_kind": c["value_kind"], + "range_group": row["range_group"], "value": c["value"], "value_raw": c["value_raw"], - "sign_convention": (c.get("conditions") or {}).get("sign_convention"), + "sign_convention": conditions.get("sign_convention"), "estimate": row["estimate"], "estimate_label": est["label"] if est else None, "pe_value": est["value"] if est else None, "gap": _round(row["gap"]), + "gap_unbounded": row["gap"] is not None and math.isinf(row["gap"]), + "group_gap": _round(row["group_gap"]), + "sign_disagrees": row["sign_disagrees"], + "over_threshold": row["over"], "nearest_estimate": row["nearest"], "nearest_pe_value": estimates[row["nearest"]]["value"] if row["nearest"] else None, + "nearest_gap": _round(row["nearest_gap"]), "status": row["status"], "cause": causes.get(row["annotation"]), "pe_missing": c.get("pe_missing"), @@ -1152,8 +1524,9 @@ def build_index( "Every PolicyEngine analysis whose comparables search is recorded in " "this repository, keyed by its sources/ directory. Generated by " "`python -m scorecard_db.comparables index` from the directories " - "themselves; policyengine-app-v2 reads it to check a research entry's " - "comparables record. A listing means the record passed the gate." + "themselves; policyengine-app-v2 and PolicyEngine/newsletters read it " + "to check an entry's comparables. A listing means the record passed " + "the gate." ), "schema": INDEX_SCHEMA, "gap_threshold": policy["gap_threshold"], @@ -1163,7 +1536,8 @@ def build_index( def render_index(index: dict) -> str: - return json.dumps(index, indent=1, sort_keys=False, ensure_ascii=False) + "\n" + # NaN and infinity are not JSON; another repository's JSON.parse would throw + return json.dumps(index, indent=1, ensure_ascii=False, allow_nan=False) + "\n" # --- the app-v2 record ------------------------------------------------------------ @@ -1171,17 +1545,11 @@ def render_index(index: dict) -> str: def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: """The ``comparables`` block for the analysis's apps.json / posts.json entry.""" - searches = [ - s - for s in record.search_log["searches"] - if s["kind"] in ("prepublish", "backfill") - ] + searches = [s for s in record.search_log["searches"] if s["kind"] in FIRST_KINDS] if search_date: searches = [s for s in searches if s["date"] == search_date] if not searches: - raise ValueError( - f"{record.name}: no prepublish or backfill search on {search_date}" - ) + raise ValueError(f"{record.name}: no prepublish or backfill search on {search_date}") summary = _search_summary(searches[-1]) return { "status": "reviewed", @@ -1197,17 +1565,17 @@ def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: # --- scaffolding ----------------------------------------------------------------- -def scaffold( - country: str, kind: str, slug: str, today: str, root: Path = SOURCES -) -> Path: +def scaffold(country: str, kind: str, slug: str, today: str, root: Path = SOURCES) -> Path: """A skeleton directory for a new search, with every producer to log.""" producers = load_producers() country = country.upper() - directory = root / f"{DIR_PREFIX}{country.lower()}-{slug}" - if directory.exists(): + if country not in producers["countries"]: raise SystemExit( - f"{directory} exists; add a search to its search_log.json instead" + f"no producer list for {country}; add one to data/comparables/producers.json first" ) + directory = root / f"{DIR_PREFIX}{country.lower()}-{slug}" + if directory.exists(): + raise SystemExit(f"{directory} exists; add a search to its search_log.json instead") directory.mkdir(parents=True) analysis = { "schema": ANALYSIS_SCHEMA, @@ -1231,6 +1599,7 @@ def scaffold( "date": today, "kind": "prepublish", "by": "", + "results_pins": [], "producers": [ { "producer": p["slug"], @@ -1245,65 +1614,81 @@ def scaffold( } ], } - (directory / "analysis.json").write_text( - json.dumps(analysis, indent=1, ensure_ascii=False) + "\n" - ) - (directory / "search_log.json").write_text( - json.dumps(log, indent=1, ensure_ascii=False) + "\n" - ) + (directory / "analysis.json").write_text(json.dumps(analysis, indent=1, ensure_ascii=False) + "\n") + (directory / "search_log.json").write_text(json.dumps(log, indent=1, ensure_ascii=False) + "\n") (directory / "manifest.jsonl").write_text("") (directory / "claims_staged.jsonl").write_text("") (directory / "annotations.json").write_text("[]\n") return directory -# --- re-harvest support ---------------------------------------------------------- +# --- registry entries and re-harvest support --------------------------------------- + +def post_slug(filename: str) -> str: + """The website's post slug (website/src/data/posts/postTransformers.ts): + the file name up to its first dot, lowercased, underscores to hyphens.""" + stem = filename[: filename.index(".")] if "." in filename else filename + return stem.lower().replace("_", "-") -def _post_slug(filename: str) -> str: - # website/src/data/posts/postTransformers.ts - return filename.rsplit(".", 1)[0].lower().replace("_", "-") + +def may_declare_no_estimate(kind: str, tags: list[str]) -> bool: + """Only a post that is plainly not an analysis (an event, an announcement) + may say it publishes no estimate; an app never may.""" + tags = set(tags) + return kind == "post" and bool(tags & NO_ESTIMATE_TAGS) and not tags & ANALYSIS_TAGS def research_entries(apps: list[dict], posts: list[dict]) -> list[dict]: - """policyengine.org research entries with a date, as {kind, country_id, - slug, date, title, authors, tags}. A post can carry several country tags; - it is listed once per country it is tagged with.""" + """policyengine.org entries the comparables rules cover: apps listed with + research or tagged policy, and every post. A post is one entry whatever its + country tags; the website serves it under every country.""" out = [] for app in apps: - if not app.get("displayWithResearch") or not app.get("date"): + if not app.get("slug") or not app.get("countryId"): + continue + tags = app.get("tags", []) + if not (app.get("displayWithResearch") or "policy" in tags): continue out.append( { "kind": "app", - "country_id": app["countryId"], + "country_ids": [app["countryId"]], "slug": app["slug"], - "date": app["date"][:10], + "date": (app.get("date") or "")[:10] or None, "title": app.get("title", ""), "authors": app.get("authors", []), - "tags": app.get("tags", []), + "tags": tags, "comparables": app.get("comparables"), } ) for post in posts: - for country_id in [ - t for t in post.get("tags", []) if t in ("uk", "us", "ca", "ng", "il") - ]: - out.append( - { - "kind": "post", - "country_id": country_id, - "slug": _post_slug(post["filename"]), - "date": post["date"][:10], - "title": post.get("title", ""), - "authors": post.get("authors", []), - "tags": post.get("tags", []), - "comparables": post.get("comparables"), - } - ) + tags = post.get("tags", []) + countries = [t for t in tags if t in COUNTRY_TAGS] or [DEFAULT_COUNTRY] + out.append( + { + "kind": "post", + "country_ids": countries, + "slug": post_slug(post["filename"]), + "date": (post.get("date") or "")[:10] or None, + "title": post.get("title", ""), + "authors": post.get("authors", []), + "tags": tags, + "comparables": post.get("comparables"), + } + ) return out +def publishes_estimate(entry: dict) -> bool: + record = entry.get("comparables") or {} + if record.get("status") == "no_estimate" and may_declare_no_estimate( + entry["kind"], entry["tags"] + ): + return False + return record.get("status") == "reviewed" or bool(set(entry["tags"]) & ANALYSIS_TAGS) + + def due( apps: list[dict], posts: list[dict], @@ -1312,19 +1697,21 @@ def due( root: Path = SOURCES, ) -> list[dict]: """Research entries published within the re-harvest window that publish an - estimate (a policy tag, or a reviewed comparables record), with the - directory that holds (or will hold) their record.""" + estimate, with the directory that holds (or will hold) their record.""" start = (dt.date.fromisoformat(today) - dt.timedelta(days=window_days)).isoformat() rows = [] + sources: dict[str, str] = {} for e in research_entries(apps, posts): - if not (start <= e["date"] <= today): + if not e["date"] or not (start <= e["date"] <= today) or not publishes_estimate(e): continue record = e.get("comparables") or {} - if record.get("status") == "no_estimate": - continue - if "policy" not in e["tags"] and record.get("status") != "reviewed": - continue - name = f"{DIR_PREFIX}{e['country_id']}-{e['slug']}" + candidates = [f"{DIR_PREFIX}{c}-{e['slug']}" for c in e["country_ids"]] + name = record.get("scorecardSource") if record.get("status") == "reviewed" else None + name = name or next((n for n in candidates if (root / n).is_dir()), candidates[0]) + label = f"{e['kind']} {e['slug']}" + if name in sources and sources[name] != label: + raise ValueError(f"{sources[name]} and {label} would share the record {name}") + sources[name] = label rows.append( { **{k: v for k, v in e.items() if k != "comparables"}, @@ -1337,25 +1724,30 @@ def due( def fresh(entry: dict, on: str, window_days: int) -> list[str]: """Why an analysis may not be promoted on date ``on`` (a newsletter, social - campaign, webinar or press push), read from its index entry: no search in - the ``window_days`` up to and including ``on``, or a gap found by then that - is still pending. Empty when it may. The newsletters repository applies the - same rule to every analysis an edition links.""" + campaign, webinar or press push), read from its index entry. It needs a + search in the ``window_days`` up to and including ``on`` that compared the + results files the page reads now, and no gap found by then still pending. + Empty when it may. PolicyEngine/newsletters applies the same rule; the + shared cases are tests/fixtures/comparables_fresh_cases.json.""" start = (dt.date.fromisoformat(on) - dt.timedelta(days=window_days)).isoformat() reasons = [] + pins = sorted(entry.get("current_pins") or []) recent = [s for s in entry["searches"] if start <= s["date"] <= on] if not recent: - last = max( - (s["date"] for s in entry["searches"] if s["date"] <= on), default=None - ) + last = max((s["date"] for s in entry["searches"] if s["date"] <= on), default=None) reasons.append( f"no comparables search between {start} and {on} (last: {last or 'none'}); " "run a prepromotion search before the push" ) + elif not any(sorted(s.get("results_pins") or []) == pins for s in recent): + reasons.append( + f"no search between {start} and {on} compared the results the page reads " + "now; run a prepromotion search against them" + ) pending = [ c["claim"] for c in entry.get("comparisons", []) - if c["status"] == "pending" and c["found_on"] <= on + if c.get("status") == "pending" and c.get("found_on", "") <= on ] if pending: reasons.append( @@ -1380,6 +1772,8 @@ def report(record: AnalysisRecord, since: str, policy: dict | None = None) -> di "source", "estimate", "gap", + "group_gap", + "sign_disagrees", "status", "annotation", "tracking_issue", @@ -1394,39 +1788,39 @@ def report(record: AnalysisRecord, since: str, policy: dict | None = None) -> di # --- CLI ------------------------------------------------------------------------------ -def _today() -> str: - return dt.datetime.now(dt.timezone.utc).date().isoformat() +def _local_today() -> str: + # the stager's own date: never later than UTC's in the Americas, so a + # scaffolded search is never dated tomorrow + return dt.date.today().isoformat() def _main(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog="python -m scorecard_db.comparables") sub = parser.add_subparsers(dest="cmd", required=True) p_check = sub.add_parser("check", help="validate analysis directories") - p_check.add_argument( - "dirs", nargs="*", help="directories (default: every sources/analysis-*)" - ) + p_check.add_argument("dirs", nargs="*", help="directories (default: every sources/analysis-*)") p_index = sub.add_parser("index", help="write data/comparables/index.json") - p_index.add_argument( - "--check", action="store_true", help="fail if the committed index is stale" - ) + p_index.add_argument("--check", action="store_true", help="fail if the committed index is stale") p_scaffold = sub.add_parser("scaffold", help="start a record for a new analysis") p_scaffold.add_argument("--country", required=True) p_scaffold.add_argument("--kind", required=True, choices=("app", "post")) p_scaffold.add_argument("--slug", required=True) - p_scaffold.add_argument("--today", default=_today()) + p_scaffold.add_argument("--today", default=_local_today()) + p_pins = sub.add_parser("pins", help="print the results_pins a new search records") + p_pins.add_argument("source") p_record = sub.add_parser("record", help="print the app-v2 comparables block") p_record.add_argument("source") p_record.add_argument("--search-date") p_due = sub.add_parser("due", help="analyses due for re-harvest") p_due.add_argument("--apps", required=True) p_due.add_argument("--posts", required=True) - p_due.add_argument("--today", default=_today()) + p_due.add_argument("--today", default=today_utc()) p_report = sub.add_parser("report", help="comparables found since a date") p_report.add_argument("source") p_report.add_argument("--since", required=True) p_fresh = sub.add_parser("fresh", help="may the analysis be promoted on a date?") p_fresh.add_argument("source") - p_fresh.add_argument("--on", default=_today()) + p_fresh.add_argument("--on", default=today_utc()) args = parser.parse_args(argv) if args.cmd == "check": @@ -1447,9 +1841,7 @@ def _main(argv: list[str]) -> int: text = render_index(build_index()) if args.check: if not INDEX_PATH.exists() or INDEX_PATH.read_text() != text: - print( - f"{INDEX_PATH} is stale: run python -m scorecard_db.comparables index" - ) + print(f"{INDEX_PATH} is stale: run python -m scorecard_db.comparables index") return 1 print(f"{INDEX_PATH} is current") return 0 @@ -1459,55 +1851,35 @@ def _main(argv: list[str]) -> int: if args.cmd == "scaffold": print(scaffold(args.country, args.kind, args.slug, args.today)) return 0 - if args.cmd == "record": - record = load_analysis(SOURCES / args.source) - errors = validate(record) - if errors: - print("\n".join(errors), file=sys.stderr) - return 1 - print( - json.dumps( - app_record(record, args.search_date), indent=2, ensure_ascii=False - ) - ) - return 0 - if args.cmd == "due": - apps = json.loads(Path(args.apps).read_text()) - posts = json.loads(Path(args.posts).read_text()) - window = load_policy()["reharvest_window_days"] - print( - json.dumps( - due(apps, posts, args.today, window), indent=1, ensure_ascii=False - ) - ) - return 0 - if args.cmd == "report": + if args.cmd == "pins": record = load_analysis(SOURCES / args.source) - errors = validate(record) - if errors: - print("\n".join(errors), file=sys.stderr) - return 1 - print( - json.dumps( - report(record, args.since), indent=1, ensure_ascii=False, default=str - ) - ) + print(json.dumps(current_pins(record.analysis), indent=1)) return 0 - if args.cmd == "fresh": + if args.cmd in ("record", "report", "fresh"): record = load_analysis(SOURCES / args.source) policy = load_policy() errors = validate(record, policy=policy) if errors: print("\n".join(errors), file=sys.stderr) return 1 - reasons = fresh( - index_entry(record, policy), args.on, policy["promotion_window_days"] - ) + if args.cmd == "record": + print(json.dumps(app_record(record, args.search_date), indent=2, ensure_ascii=False)) + return 0 + if args.cmd == "report": + print(json.dumps(report(record, args.since, policy), indent=1, ensure_ascii=False, default=str)) + return 0 + reasons = fresh(index_entry(record, policy), args.on, policy["promotion_window_days"]) for reason in reasons: print(f"NOT FRESH {args.source}: {reason}") if not reasons: print(f"fresh {args.source} on {args.on}") return 1 if reasons else 0 + if args.cmd == "due": + apps = _loads(Path(args.apps).read_text()) + posts = _loads(Path(args.posts).read_text()) + window = load_policy()["reharvest_window_days"] + print(json.dumps(due(apps, posts, args.today, window), indent=1, ensure_ascii=False)) + return 0 return 2 diff --git a/scorecard_db/export_populations.py b/scorecard_db/export_populations.py index 0d3d0e6d2..dbf9648a5 100644 --- a/scorecard_db/export_populations.py +++ b/scorecard_db/export_populations.py @@ -174,6 +174,7 @@ def export( } rows = [] by_source: dict[str, int] = {} + by_analysis_source: dict[str, int] = {} by_status: dict[str, int] = {} by_sign: dict[str, int] = {} multi_release = 0 @@ -244,7 +245,15 @@ def export( ) if len({r["data_bundle"] for r in results}) > 1: multi_release += 1 - by_source[c["source"]] = by_source.get(c["source"], 0) + 1 + # A comparables record re-stages a producer's figure for one analysis + # (docs/COMPARABLES.md); counting it again per source would count the + # producer-level claim twice, so those claims are tallied apart. + tally = ( + by_analysis_source + if publication.get("registry") == "analysis_comparables" + else by_source + ) + tally[c["source"]] = tally.get(c["source"], 0) + 1 eff = latest["status_effective"] by_status[eff] = by_status.get(eff, 0) + 1 # issue #198: the published value's sign against PolicyEngine's @@ -382,6 +391,9 @@ def export( "claims": len(rows), "multi_release_claims": multi_release, "by_source": dict(sorted(by_source.items())), + "by_source_in_comparables_records": dict( + sorted(by_analysis_source.items()) + ), "by_latest_status": dict(sorted(by_status.items())), "by_sign_relative_to_pe": dict(sorted(by_sign.items())), "by_country": { diff --git a/scorecard_db/ingest_analysis_comparables.py b/scorecard_db/ingest_analysis_comparables.py index 0f582b942..b026939e5 100644 --- a/scorecard_db/ingest_analysis_comparables.py +++ b/scorecard_db/ingest_analysis_comparables.py @@ -1,4 +1,4 @@ -"""Ingest every pre-publish comparables record (docs/COMPARABLES.md). +"""Ingest every comparables record (docs/COMPARABLES.md). Each ``sources/analysis--/`` directory records the external estimates found for one PolicyEngine analysis and the analysis's own @@ -10,11 +10,17 @@ ``conditions.analysis`` naming the analysis so its claims are retrievable together and never collide with a producer-level source staging the same publication; -- every compared claim gets one PolicyEngine result: the analysis's own - published figure, read from the results file analysis.json pins, with the - comparison status its annotation gives (``comparable`` when none does); -- a claim with no PolicyEngine counterpart is published without a result, - and ``conditions.pe_missing`` says why. +- a compared claim gets the analysis's own figure as its result, read from + the results file analysis.json pins, with the status its annotation gives + (``comparable``, or ``constructed`` across two worlds, when none does; a + gap still pending on its issue keeps that status and says so); +- an uncompared claim set beside its nearest estimate gets that figure as + its result, labelled with the mismatch its annotation states, so the + number is never hidden; +- earlier figures of the same estimate (``superseded_estimates``) are kept + as older results on the same claim: history, not state; +- a claim with no PolicyEngine figure near it is published without a + result, and ``conditions.pe_missing`` says why. Rows the database cannot hold are tallied, not dropped silently: a claim with a proposed metric or unit (harvest rule 2) waits for its vocabulary @@ -69,6 +75,29 @@ FIRST_SEARCH_DATE = "2026-10-21" +def _value_kind(unit: str) -> str: + """ExternalScore.value_kind for a unit concept (count | share | percent | + index | usd | gbp | eur | nzd), as the other ingests set it.""" + if unit.startswith("gbp"): + return "gbp" + if unit.startswith("usd"): + return "usd" + if unit in ("eur", "nzd", "share"): + return unit + if unit.startswith("percent"): + return "percent" + if unit == "index_0_1": + return "index" + return "count" + + +_PLURALS = {"analysis": "analyses"} + + +def _plural(n: int, word: str) -> str: + return f"{n} {word if n == 1 else _PLURALS.get(word, word + 's')}" + + def _baseline_descriptor(label: str | None) -> dict | None: if label is None: return None @@ -148,6 +177,7 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] if c["value_kind"] != "point": conditions["estimate_kind"] = c["value_kind"] with_baseline_condition(conditions, reform) + row = evaluation[c["key"]] if not c.get("compare_to"): conditions["pe_missing"] = c["pe_missing"] publication = { @@ -162,7 +192,7 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] "country": a["country"], "analysis_url": a["url"], } - if not c.get("compare_to"): + if not row["estimate"] and not row["nearest"]: publication["publish_without_result"] = True score = ExternalScore( source=c["source"], @@ -177,12 +207,11 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] source_model=c.get("source_model"), source_column=c["source_column"], publication=publication, - value_kind=c["unit_concept"], + value_kind=_value_kind(c["unit_concept"]), status="ok", ) scores.append(score) t["claims"] += 1 - row = evaluation[c["key"]] if not row["estimate"] and not row["nearest"]: t["not_compared"] += 1 continue @@ -198,42 +227,54 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] if note: annotations.append(f"{note['id']}: {note['cause']}") if status == "pending": - # the comparison is like for like; its gap is open on the owner's issue - status = ComparisonStatus.COMPARABLE.value + # pending is a state of the explanation, not of the + # comparison: the comparison keeps its own status + status = ( + ComparisonStatus.CONSTRUCTED.value + if c["world"] != est["world"] + else ComparisonStatus.COMPARABLE.value + ) annotations.append( f"gap unexplained, tracked at {note['tracking_issue']}" ) world_baseline = a["worlds"].get(est["world"], {}).get("baseline") executed = _baseline_descriptor(world_baseline) or CURRENT_LAW_DESCRIPTOR - prov = est["provenance"] - results.append( - PEResult( - claim_id=score.claim_id(), - computed_value=float(est["value"]), - status=ComparisonStatus(status), - engine_version=_engine_version(est), - data_bundle=prov["dataset"], - pe_construction=json.dumps( - { - "analysis": a["id"], - "estimate": est["key"], - "repo": prov["repo"], - "commit": prov["commit"], - "path": prov["path"], - "sha256": prov["sha256"], - "field": prov["field"], - "relation": row["relation"], - "compared": bool(row["estimate"]), - }, - sort_keys=True, - ), - run_id=f"analysis:{a['id']}@{prov['commit'][:7]}", - computed_at=prov["computed_at"], - annotations=annotations, - baseline_key=baseline_key(executed), + history = [ + e + for e in a.get("superseded_estimates") or [] + if e["key"] == est["key"] + ] + for version in [est, *history]: + prov = version["provenance"] + results.append( + PEResult( + claim_id=score.claim_id(), + computed_value=float(version["value"]), + status=ComparisonStatus(status), + engine_version=_engine_version(version), + data_bundle=prov["dataset"], + pe_construction=json.dumps( + { + "analysis": a["id"], + "estimate": version["key"], + "repo": prov["repo"], + "commit": prov["commit"], + "path": prov["path"], + "sha256": prov["sha256"], + "field": prov["field"], + "relation": row["relation"], + "compared": bool(row["estimate"]), + "superseded_on": version.get("superseded_on"), + }, + sort_keys=True, + ), + run_id=f"analysis:{a['id']}@{prov['commit'][:7]}", + computed_at=prov["computed_at"], + annotations=annotations, + baseline_key=baseline_key(executed), + ) ) - ) - t["results"] += 1 + t["results"] += 1 return finish(scores, REGISTRY), results, tally @@ -288,8 +329,9 @@ def ingest(db_path: Path) -> dict: else ("ingested" if t["claims"] else "registered") ) detail = ( - f"{t['analyses']} analyses searched, {t['claims']} claims, " - f"{t['results']} with the analysis's own figure beside them" + f"{_plural(t['analyses'], 'analysis')} searched, " + f"{_plural(t['claims'], 'claim')}, " + f"{_plural(t['results'], 'PolicyEngine result')} beside them" ) db.conn.execute(LANE_SQL, (lane, stage_name, detail, updated)) sync_lane_feed(db, REPO / "data" / "lanes.json", updated, lanes=LANE_FEED_META) diff --git a/sources/analysis-uk-childcare-100k-threshold/analysis.json b/sources/analysis-uk-childcare-100k-threshold/analysis.json index 4252a9034..8ec4464de 100644 --- a/sources/analysis-uk-childcare-100k-threshold/analysis.json +++ b/sources/analysis-uk-childcare-100k-threshold/analysis.json @@ -14,7 +14,7 @@ "author": "vahid-ahmadi" }, "scope": "uk_wide", - "subject": "The cost of removing the £100,000 adjusted-net-income limit on the 30 funded hours for working parents (England) and on Tax-Free Childcare (UK-wide), who gains, and the cliff it removes.", + "subject": "The cost of removing the £100,000 adjusted-net-income limit on the 30 funded hours for working parents (England) and on Tax-Free Childcare (UK-wide), static and net of the labour-supply response, who gains, and the cliff it removes.", "search_terms": [ "£100,000 childcare cliff edge", "100k childcare limit cost", @@ -296,6 +296,926 @@ }, "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" } + }, + { + "key": "both_schemes_static_range_low_fy2026", + "label": "Low end of the static cost range, both schemes, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -543000000, + "value_raw": "0.543", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_high_fy2026", + "label": "High end of the static cost range, both schemes, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -1160000000, + "value_raw": "1.16", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_low_fy2027", + "label": "Low end of the static cost range, both schemes, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -578000000, + "value_raw": "0.578", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_high_fy2027", + "label": "High end of the static cost range, both schemes, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -1240000000, + "value_raw": "1.24", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_low_fy2028", + "label": "Low end of the static cost range, both schemes, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -635000000, + "value_raw": "0.635", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_high_fy2028", + "label": "High end of the static cost range, both schemes, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -1371000000, + "value_raw": "1.371", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_low_fy2029", + "label": "Low end of the static cost range, both schemes, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -652000000, + "value_raw": "0.652", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_static_range_high_fy2029", + "label": "High end of the static cost range, both schemes, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -1433000000, + "value_raw": "1.433", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_central_fy2026", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -451000000, + "value_raw": "0.451", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_central_fy2027", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -479000000, + "value_raw": "0.479", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_central_fy2028", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -533000000, + "value_raw": "0.533", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_central_fy2029", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -556000000, + "value_raw": "0.556", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_low_fy2026", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -523000000, + "value_raw": "0.523", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_low_fy2027", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -555000000, + "value_raw": "0.555", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_low_fy2028", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -613000000, + "value_raw": "0.613", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_low_fy2029", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -637000000, + "value_raw": "0.637", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_high_fy2026", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -344000000, + "value_raw": "0.344", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_high_fy2027", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -365000000, + "value_raw": "0.365", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_high_fy2028", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -415000000, + "value_raw": "0.415", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "both_schemes_dynamic_high_fy2029", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -432000000, + "value_raw": "0.432", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_fy2026", + "label": "Families gaining, 2026-27", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2027, + "time_basis": "fiscal_year", + "value": 270000, + "value_raw": "270000", + "normalization": "as printed", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2026.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_thirty_hours_fy2026", + "label": "Families gaining through the 30 funded hours, 2026-27", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2027, + "time_basis": "fiscal_year", + "value": 87000, + "value_raw": "87000", + "normalization": "as printed", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2026.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_fy2027", + "label": "Families gaining, 2027-28", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2028, + "time_basis": "fiscal_year", + "value": 278000, + "value_raw": "278000", + "normalization": "as printed", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2027.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_thirty_hours_fy2027", + "label": "Families gaining through the 30 funded hours, 2027-28", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2028, + "time_basis": "fiscal_year", + "value": 90000, + "value_raw": "90000", + "normalization": "as printed", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2027.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_fy2028", + "label": "Families gaining, 2028-29", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2029, + "time_basis": "fiscal_year", + "value": 297000, + "value_raw": "297000", + "normalization": "as printed", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2028.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_thirty_hours_fy2028", + "label": "Families gaining through the 30 funded hours, 2028-29", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2029, + "time_basis": "fiscal_year", + "value": 98000, + "value_raw": "98000", + "normalization": "as printed", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2028.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_fy2029", + "label": "Families gaining, 2029-30", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2030, + "time_basis": "fiscal_year", + "value": 304000, + "value_raw": "304000", + "normalization": "as printed", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2029.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_thirty_hours_fy2029", + "label": "Families gaining through the 30 funded hours, 2029-30", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2030, + "time_basis": "fiscal_year", + "value": 101000, + "value_raw": "101000", + "normalization": "as printed", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2029.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } } ] } diff --git a/sources/analysis-uk-childcare-100k-threshold/annotations.json b/sources/analysis-uk-childcare-100k-threshold/annotations.json index 2141d2890..d2a7205ac 100644 --- a/sources/analysis-uk-childcare-100k-threshold/annotations.json +++ b/sources/analysis-uk-childcare-100k-threshold/annotations.json @@ -7,11 +7,12 @@ "centax_static_high_cost_fy2027", "centax_static_high_cost_fy2029", "centax_static_low_cost_fy2027", - "centax_static_low_cost_fy2029" + "centax_static_low_cost_fy2029", + "centax_static_central_fy2028" ], "status": "constructed", "explains_gap": true, - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "basis": [ { "kind": "issue", @@ -23,12 +24,12 @@ }, { "kind": "issue", - "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/pull/8" + "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/pull/8#issuecomment-6337369324" } ] }, { - "id": "net-of-behaviour-vs-static", + "id": "centax-net-vs-dashboard-dynamic", "claims": [ "centax_net_central_fy2027", "centax_net_central_fy2029", @@ -36,6 +37,25 @@ "centax_net_high_cost_fy2029", "centax_net_low_cost_fy2027", "centax_net_low_cost_fy2029", + "centax_net_central_fy2028" + ], + "status": "concept_mismatch", + "explains_gap": false, + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "basis": [ + { + "kind": "document", + "ref": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf#page=42" + }, + { + "kind": "engine_metadata", + "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/blob/f25f0dbd46c31d336bde06898d801a1887a8cf89/data/results.json" + } + ] + }, + { + "id": "cbp-bunching-vs-labour-supply", + "claims": [ "cbp_net_table2_fy2026", "cbp_net_table2_fy2027", "cbp_net_table2_fy2028", @@ -45,21 +65,25 @@ "cbp_net_table3_fy2028", "cbp_net_table3_fy2029" ], - "status": "concept_mismatch", - "explains_gap": false, - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", + "status": "comparable", + "explains_gap": true, + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", "basis": [ { "kind": "document", - "ref": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf#page=42" + "ref": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf#page=8" }, { "kind": "document", - "ref": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf#page=8" + "ref": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf#page=10" }, { "kind": "engine_metadata", "ref": "https://github.com/PolicyEngine/uk-childcare-100k-threshold/blob/f25f0dbd46c31d336bde06898d801a1887a8cf89/data/results.json" + }, + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/microcosm/issues/1126" } ] } diff --git a/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl index 2033550a9..ad42fa4ba 100644 --- a/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl +++ b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl @@ -1,20 +1,22 @@ -{"key": "centax_static_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -800000000, "value_raw": "-800m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Static Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -520000000, "value_raw": "-520m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "centax_static_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -980000000, "value_raw": "-980m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Static Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -640000000, "value_raw": "-640m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "centax_static_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -820000000, "value_raw": "-820m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -610000000, "value_raw": "-610m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "centax_static_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -1010000000, "value_raw": "-1,010m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -770000000, "value_raw": "-770m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "centax_static_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -580000000, "value_raw": "-580m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 140000000, "value_raw": "+140m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2027", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "centax_static_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -700000000, "value_raw": "-700m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "static", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} -{"key": "centax_net_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 160000000, "value_raw": "+160m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "net of intensive and extensive behavioural margins", "geography": "England", "program": "free_childcare_hours", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "thirty_hours_static_fy2029", "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition."} -{"key": "cbp_net_table2_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 70000000, "value_raw": "0.070", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2026", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 77000000, "value_raw": "0.077", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2027", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 91000000, "value_raw": "0.091", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2028", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table2_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 132000000, "value_raw": "0.132", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2029", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 330000000, "value_raw": "0.330", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2026", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 349000000, "value_raw": "0.349", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2027", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 399000000, "value_raw": "0.399", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2028", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} -{"key": "cbp_net_table3_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 427000000, "value_raw": "0.427", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "net of behavioural responses", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff: abolish the £100,000 cliff on Free Childcare for Working Parents and Tax-Free Childcare", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": null, "nearest_estimate": "both_schemes_static_fy2029", "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "parse_confidence": "high", "status": "ok"} +{"key": "centax_static_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -800000000, "value_raw": "-800m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Static Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_central_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -520000000, "value_raw": "-520m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "central", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 13,360 3,850 -800m +150m +120m -520m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2027", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -980000000, "value_raw": "-980m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Static Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_central_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -640000000, "value_raw": "-640m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "central", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 16,280 4,300 -980m +210m +130m -640m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2029", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -820000000, "value_raw": "-820m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_low", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_static_fy2027_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_high_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -610000000, "value_raw": "-610m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_low", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "high_cost", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_net_fy2027_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 9,710 3,850 -820m +100m +120m -610m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2027", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -1010000000, "value_raw": "-1,010m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_low", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_static_fy2029_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_high_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -770000000, "value_raw": "-770m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_low", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "high_cost", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_net_fy2029_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 10,860 4,300 -1,010m +120m +120m -770m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2029", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -580000000, "value_raw": "-580m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_high", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_static_fy2027_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2027", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_low_cost_fy2027", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 140000000, "value_raw": "+140m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_high", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "scenario": "low_cost", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_net_fy2027_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (£)", "quote": "2028 58,510 3,850 -580m +630m +90m +140m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2027", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -700000000, "value_raw": "-700m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_high", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_static_fy2029_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2029", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_low_cost_fy2029", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": 160000000, "value_raw": "+160m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "range_high", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "scenario": "low_cost", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "range_group": "centax_net_fy2029_scenarios"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table 4.2, printed page 41 (PDF page 42)", "access": "direct"}, "source_table": "Table 4.2", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (£)", "quote": "2030 67,650 4,300 -700m +770m +90m +160m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2029", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "centax_static_central_fy2028", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -880000000, "value_raw": "-880m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "scenario": "central", "basis": "static", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table B.1 (actual ANI, central scenario), printed page 82 (PDF page 83)", "access": "direct"}, "source_table": "Table B.1", "source_column": "Central Scenario, tax year 2029: Static Cost (£)", "quote": "2029 4,060 -880m +180m +120m -580m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "thirty_hours_static_fy2028", "across_worlds": {"reason": "The dashboard costs the 30 hours in its both-limits world and CenTax costs the free hours alone. Removing Tax-Free Childcare's limit does not move the free-hours heads: the scorecard's differential test measured them equal in the 30-hours-only and both-limits worlds on the certified pair.", "basis": [{"kind": "diagnostic", "ref": "https://github.com/PolicyEngine/policyengine-scorecard/blob/661c14d6b907235bf402de1d54d3592a8b2fbbcd/tests/test_uk_childcare_100k.py#L271"}]}}} +{"key": "centax_net_central_fy2028", "source": "centax", "found_by": "centax", "found_on": "2026-10-21", "source_model": "hmrc_administrative_microdata", "metric": "revenue_change", "unit_concept": "gbp", "value": -580000000, "value_raw": "-580m", "normalization": "GBP m x 1e6 -> GBP", "value_kind": "central", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "scenario": "central", "basis": "post_behavioural", "program": "free_childcare_hours", "geography": "England", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')"}, "world": "free_hours_limit_removed", "reform_hint": "abolishing the threshold and providing the childcare entitlement with no limit on how high a parent's income can be before losing any entitlement", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Removing the childcare cliff-edge: impacts and cost of reform", "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "date": "2026-09-09", "publisher": "CenTax", "primary_sha256": "733b7a1d48409071135303c3363482126ead83c4d6e02d9b7f049e4d3113140a", "locator": "Table B.1 (actual ANI, central scenario), printed page 82 (PDF page 83)", "access": "direct"}, "source_table": "Table B.1", "source_column": "Central Scenario, tax year 2029: Net Fiscal Cost (£)", "quote": "2029 4,060 -880m +180m +120m -580m", "note": "CenTax labels tax years by their later year ('tax year 2021-22 is given as 2022', printed page 2), so 2028 is 2027-28.", "parse_confidence": "high", "status": "ok", "compare_to": null, "nearest_estimate": "both_schemes_dynamic_central_fy2028", "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching."} +{"key": "cbp_net_table2_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 70000000, "value_raw": "0.070", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2026"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 77000000, "value_raw": "0.077", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2027"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 91000000, "value_raw": "0.091", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2028"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table2_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 132000000, "value_raw": "0.132", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_pension_exogenous_labour", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.3.7, Table 2 (PDF page 8)", "access": "direct"}, "source_table": "Table 2: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Pension, Exogenous Labour", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.070 0.077 0.091 0.132 0.147 0.172", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2029"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2026", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 330000000, "value_raw": "0.330", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "conditions": {"fy": "2026-27", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.4.6, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2026/27", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2026"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2027", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 349000000, "value_raw": "0.349", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", "conditions": {"fy": "2027-28", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.4.6, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2027/28", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2027"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2028", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 399000000, "value_raw": "0.399", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2029, "time_basis": "fiscal_year", "conditions": {"fy": "2028-29", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.4.6, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2028/29", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2028"}, "parse_confidence": "high", "status": "ok"} +{"key": "cbp_net_table3_fy2029", "source": "centre_for_british_progress", "found_by": "centre_for_british_progress", "found_on": "2026-10-21", "source_model": "cbp_synthetic_microsimulation", "metric": "revenue_change", "unit_concept": "gbp", "value": 427000000, "value_raw": "0.427", "normalization": "GBP bn x 1e9 -> GBP", "value_kind": "point", "period": 2030, "time_basis": "fiscal_year", "conditions": {"fy": "2029-30", "model": "endogenous_labour_no_pension", "assumptions": "full utilisation of FCWP and £500 utilisation of TFC; friction parameter £1000; Frisch elasticity 0.3", "basis": "post_behavioural", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)"}, "world": "both_limits_removed", "reform_hint": "Abolish Cliff", "attribution": "own", "benchmark_class": "different_model", "baseline_policy": null, "publication": {"title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "date": "2026-03-25", "publisher": "Centre for British Progress", "primary_sha256": "53697ee68ecc07bf96d2ff387643523c97907fbcee5a7382a3c6d8bbe7774b79", "locator": "section 1.4.6, Table 3 (PDF page 10)", "access": "direct"}, "source_table": "Table 3: Projected Net Fiscal Impact of Childcare Entitlements (£bn): Endogenous Labour, No Pension", "source_column": "Abolish Cliff, 2029/30", "quote": "Abolish Cliff 0.330 0.349 0.399 0.427 0.462 0.507", "note": "Table 2's 0.132 for 2029/30 is the '£130m' net gain to the Exchequer reported in coverage of the paper (AJ Bell, March 2026). Columns 2030/31 and 2031/32 are outside the dashboard's years and are not staged.", "compare_to": {"pe_estimate": "both_schemes_dynamic_central_fy2029"}, "parse_confidence": "high", "status": "ok"} diff --git a/sources/analysis-uk-childcare-100k-threshold/search_log.json b/sources/analysis-uk-childcare-100k-threshold/search_log.json index 2b9ab21c8..79de38fab 100644 --- a/sources/analysis-uk-childcare-100k-threshold/search_log.json +++ b/sources/analysis-uk-childcare-100k-threshold/search_log.json @@ -5,6 +5,9 @@ "date": "2026-10-21", "kind": "backfill", "by": "session:local_4b58a325-2146-422b-80fa-69a117a812aa", + "results_pins": [ + "PolicyEngine/uk-childcare-100k-threshold@f25f0dbd46c31d336bde06898d801a1887a8cf89:4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a" + ], "producers": [ { "producer": "obr", @@ -112,9 +115,11 @@ "centax_static_low_cost_fy2027", "centax_net_low_cost_fy2027", "centax_static_low_cost_fy2029", - "centax_net_low_cost_fy2029" + "centax_net_low_cost_fy2029", + "centax_static_central_fy2028", + "centax_net_central_fy2028" ], - "note": "Table 4.2: three scenarios x two tax years; static and net staged, the behavioural margins and bunching counts are in sources/centax-childcare-100k." + "note": "Table 4.2 (three scenarios x 2028 and 2030) and Table B.1's 2029 row, static and net. The behavioural margins and bunching counts are in sources/centax-childcare-100k. Table B.1's 2028 and 2030 rows repeat Table 4.2's central rows; its 2031 row is outside the dashboard's years. Table B.2 (expected ANI) prints the same abolition figures." }, { "producer": "centre_for_british_progress", diff --git a/tests/test_comparables.py b/tests/test_comparables.py index bd88a14d2..1946e337b 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -1,30 +1,43 @@ -"""The pre-publish comparables gate (docs/COMPARABLES.md). +"""The comparables gate (docs/COMPARABLES.md). Invariants, each tested below: - Completeness: every committed sources/analysis-* directory passes the validator, and the committed index is exactly what the directories build (twice, identically). -- The gate: a compared claim whose gap exceeds the threshold fails unless an - annotation explains it, for any PolicyEngine and external values; within the - threshold it passes with no annotation (Hypothesis). +- The gate: a compared claim fails exactly when its gap exceeds the threshold + or its sign disagrees, unless an annotation explains it, for values of either + sign and orientation (Hypothesis); an explained gap always passes. +- No evasion: no relabelling of a like-for-like claim (its basis, programme, + time basis, period, metric, value kind, status or annotation) gets an + unexplained gap past the gate without the validator saying so. +- Search integrity: a search's kind and date must fit the record (no search + after today, before the producer list, a later-kind search on its first day, + or a prepublish search after publication); a pending gap only on what a + post-publication search found; the latest search compared the results files + the estimates are read from now. - The gap rule: zero for equal values, invariant to rescaling both values, - orientation-consistent (an opposite-signed convention equals the same value - negated), sign-free for magnitudes, monotone in the threshold, and over any - threshold when a nonzero estimate meets a published zero. -- Harvest rule 1: a printed value parses back to the staged value, in every - printed form (Hypothesis), and a re-signed or mis-scaled value fails. + orientation-consistent, sign-free only for unsigned levels, over every + allowed threshold when signs disagree, monotone in the threshold, and over + any threshold when a nonzero estimate meets a published zero. A range is + over only when the estimate lies outside it by more than the threshold. +- Harvest rule 1: a printed value parses back to the staged value at the + scale it states, in every printed form (Hypothesis); the quote prints it as + a whole number with its sign. - Coverage: a search that skips any listed producer, or calls an applicable producer not_applicable, fails; Max's 2026-10-21 producer list is pinned. -- The database: every analysis claim is held out, every compared claim has - exactly one result whose status the record decides, and every uncompared - claim says why. -- Differential: the app-v2 record and the index agree on the search they cite. +- The database: every analysis claim is held out; the claims ingested are + exactly the records' claims the database can hold; each compared claim, and + each claim beside its nearest estimate, has the analysis's figure as its + result with the record's status. +- Differential: the app-v2 block and the index cite the same search; the + freshness rule agrees with the shared cases newsletters checks too. """ from __future__ import annotations import copy +import datetime as dt import json import math import sqlite3 @@ -39,9 +52,11 @@ REPO = Path(__file__).resolve().parent.parent COMMITTED_DB = REPO / "data" / "scorecard.db" CHILDCARE = cmp.SOURCES / "analysis-uk-childcare-100k-threshold" +FRESH_CASES = REPO / "tests" / "fixtures" / "comparables_fresh_cases.json" PRODUCERS = cmp.load_producers() POLICY = cmp.load_policy() THRESHOLD = POLICY["gap_threshold"] +WINDOW = POLICY["promotion_window_days"] # Max's list of 2026-10-21. Additions are fine; removing one of these fails. MAX_LIST = { @@ -78,20 +93,21 @@ } +def shift(date: str, days: int) -> str: + return (dt.date.fromisoformat(date) + dt.timedelta(days=days)).isoformat() + + # --- a minimal synthetic record ------------------------------------------------ SHA = "a" * 64 COMMIT = "b" * 40 +PIN = f"PolicyEngine/test@{COMMIT}:{SHA}" SEARCH_DATE = "2026-10-21" +EXCHEQUER = "positive = yield to the Exchequer" -def synthetic( - pe_value: float, ext_value: float, *, annotate: bool = False -) -> cmp.AnalysisRecord: - """One UK analysis, one estimate, one compared claim; every producer - logged none_found except the one that found the claim.""" - claim_key = "ext_cost_fy2027" - producers = [] +def _producers(found_by: str, keys: list[str]) -> list[dict]: + out = [] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE): entry = { "producer": p["slug"], @@ -100,11 +116,21 @@ def synthetic( "checked": [], "claims": [], } - if p["slug"] == "ifs": - entry.update(result="found", claims=[claim_key]) + if p["slug"] == found_by: + entry.update(result="found", claims=list(keys)) if not cmp.applies(p, "uk_wide"): entry.update(result="not_applicable", queries=[]) - producers.append(entry) + out.append(entry) + return out + + +def synthetic( + pe_value: float, ext_value: float, *, annotate: bool = False +) -> cmp.AnalysisRecord: + """One UK analysis, one estimate, one compared claim; every producer + logged none_found except the one that found the claim.""" + claim_key = "ext_cost_fy2027" + identity = {"fy": "2027-28", "basis": "static", "program": "test_programme"} analysis = { "schema": cmp.ANALYSIS_SCHEMA, "id": "uk/test-analysis", @@ -118,10 +144,7 @@ def synthetic( "subject": "The cost of a test reform", "search_terms": ["test reform cost"], "worlds": { - "test_reform": { - "policy": "uk_test_reform", - "description": "A test reform world", - } + "test_reform": {"policy": "uk_test_reform", "description": "A test reform world"} }, "pe_estimates": [ { @@ -137,7 +160,7 @@ def synthetic( "normalization": "as printed", "sign": -1 if pe_value < 0 else 1, "sign_basis": "printed as a positive cost; revenue_change is positive = yield", - "conditions": {"sign_convention": "positive = yield to the Exchequer"}, + "conditions": {**identity, "sign_convention": EXCHEQUER}, "provenance": { "repo": "PolicyEngine/test", "commit": COMMIT, @@ -151,7 +174,7 @@ def synthetic( } ], } - raw = repr(abs(ext_value)) + printed = ("-" if ext_value < 0 else "") + repr(abs(ext_value)) claim = { "key": claim_key, "source": "ifs", @@ -161,12 +184,12 @@ def synthetic( "metric": "revenue_change", "unit_concept": "gbp", "value": ext_value, - "value_raw": ("-" if ext_value < 0 else "") + raw, + "value_raw": printed, "normalization": "as printed", "value_kind": "point", "period": 2028, "time_basis": "fiscal_year", - "conditions": {"sign_convention": "positive = yield to the Exchequer"}, + "conditions": {**identity, "sign_convention": EXCHEQUER}, "world": "test_reform", "reform_hint": "the test reform", "attribution": "own", @@ -183,7 +206,7 @@ def synthetic( }, "source_table": "Table 1", "source_column": "Cost", - "quote": f"Cost {('-' if ext_value < 0 else '') + raw}", + "quote": f"Cost {printed}", "parse_confidence": "high", "status": "ok", "compare_to": {"pe_estimate": "pe_cost_fy2027"}, @@ -197,12 +220,7 @@ def synthetic( "status": "comparable", "explains_gap": True, "cause": "A documented difference in the test populations explains it.", - "basis": [ - { - "kind": "issue", - "ref": "https://github.com/PolicyEngine/test/issues/1", - } - ], + "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/1"}], } ) return cmp.AnalysisRecord( @@ -215,7 +233,8 @@ def synthetic( "date": SEARCH_DATE, "kind": "prepublish", "by": "test", - "producers": producers, + "results_pins": [PIN], + "producers": _producers("ifs", [claim_key]), } ], }, @@ -243,6 +262,43 @@ def childcare() -> cmp.AnalysisRecord: return cmp.load_analysis(CHILDCARE) +def mutate(fn, base=None): + record = base or synthetic(-100.0, -105.0) + assert errors_of(record) == [] + fn(record) + return errors_of(record) + + +def claim0(record) -> dict: + return record.claims[0] + + +def note(nid, keys, status, explains=False, **extra): + return { + "id": nid, + "claims": keys, + "status": status, + "explains_gap": explains, + "cause": "a cause long enough to count as one", + "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/9"}], + **extra, + } + + +def post_publication(record, kind="reharvest", later="2026-10-22"): + """The synthetic record, published on its search day, then searched again.""" + first = copy.deepcopy(record.search_log["searches"][0]) + for p in first["producers"]: + if p["producer"] == "ifs": + p.update(result="none_found", claims=[]) + record.analysis["published"] = SEARCH_DATE + later_search = record.search_log["searches"][0] + later_search.update(date=later, kind=kind) + record.search_log["searches"] = [first, later_search] + claim0(record)["found_on"] = later + return record + + # --- completeness of what is committed --------------------------------------------- @@ -263,6 +319,13 @@ def test_index_lists_every_record_and_only_records(): index = json.loads(cmp.INDEX_PATH.read_text()) assert sorted(index["analyses"]) == [d.name for d in cmp.analysis_dirs()] assert index["gap_threshold"] == THRESHOLD + assert index["promotion_window_days"] == WINDOW + + +def test_the_index_is_strict_json(): + with pytest.raises(ValueError): + cmp.render_index({"x": float("nan")}) + json.loads(cmp.INDEX_PATH.read_text(), parse_constant=lambda c: pytest.fail(c)) def test_max_producer_list_is_kept(): @@ -274,20 +337,22 @@ def test_max_producer_list_is_kept(): def test_every_producer_is_complete(): for country, producers in PRODUCERS["countries"].items(): for p in producers: - assert ( - p["name"] and p["kind"] and p["search_hints"] and p["where_to_look"] - ), p["slug"] + assert p["name"] and p["kind"] and p["search_hints"] and p["where_to_look"], p["slug"] if p["slug"] not in MAX_LIST[country] and p["slug"] != "open_search": - assert p.get("why"), ( - f"{p['slug']} was added beyond Max's list without a reason" - ) + assert p.get("why"), f"{p['slug']} was added beyond Max's list without a reason" + + +def test_a_producer_cannot_be_added_in_the_future(tmp_path): + data = json.loads(cmp.PRODUCERS_PATH.read_text()) + data["countries"]["UK"][0]["added"] = "2099-01-01" + path = tmp_path / "producers.json" + path.write_text(json.dumps(data)) + with pytest.raises(ValueError, match="no later than today"): + cmp.load_producers(path) def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): - assert ( - cmp.producer_for_source(PRODUCERS, "US", "ca_fiscal_note") - == "state_fiscal_note" - ) + assert cmp.producer_for_source(PRODUCERS, "US", "ca_fiscal_note") == "state_fiscal_note" assert cmp.producer_for_source(PRODUCERS, "UK", "obr_efo") == "obr" assert cmp.producer_for_source(PRODUCERS, "UK", "unlisted_tank") is None @@ -297,30 +362,30 @@ def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): def test_childcare_gap_fails_the_gate_without_its_explanation(): record = childcare() - record.annotations = [ - n for n in record.annotations if n["id"] != "hours-draw-microcosm-1126" - ] - errors = errors_of(record) - unexplained = [e for e in errors if "no annotation explains it" in e] - assert len(unexplained) == 6 + record.annotations = [n for n in record.annotations if n["id"] != "hours-draw-microcosm-1126"] + unexplained = [e for e in errors_of(record) if "no annotation explains it" in e] + # three central years, and the two scenario ranges + assert len(unexplained) == 7 assert any("centax_static_central_fy2027 is -45.8%" in e for e in unexplained) -def test_childcare_central_gap_is_what_the_published_figures_imply(): +def test_childcare_figures_are_what_the_published_files_imply(): rows = {r["key"]: r for r in cmp.evaluate(childcare(), POLICY)} - assert rows["centax_static_central_fy2027"]["gap"] == pytest.approx( - 0.434 / 0.80 - 1 - ) - assert rows["centax_static_central_fy2029"]["gap"] == pytest.approx( - 0.505 / 0.98 - 1 - ) + assert rows["centax_static_central_fy2027"]["gap"] == pytest.approx(0.434 / 0.80 - 1) + assert rows["centax_static_central_fy2029"]["gap"] == pytest.approx(0.505 / 0.98 - 1) + assert rows["centax_static_central_fy2028"]["gap"] == pytest.approx(0.484 / 0.88 - 1) + # the scenario range is over by its nearer bound, the low-cost scenario + assert rows["centax_static_low_cost_fy2027"]["group_gap"] == pytest.approx(0.434 / 0.58 - 1) # compared across two worlds, so constructed rather than comparable assert rows["centax_static_central_fy2027"]["status"] == "constructed" - assert rows["centax_net_central_fy2027"]["estimate"] is None - # net of behaviour: beside the static figure, labelled a concept mismatch - assert rows["centax_net_central_fy2027"]["nearest"] == "thirty_hours_static_fy2027" + # net of behaviour beside the dashboard's net figure, labelled a mismatch + assert rows["centax_net_central_fy2027"]["nearest"] == "both_schemes_dynamic_central_fy2027" assert rows["centax_net_central_fy2027"]["status"] == "concept_mismatch" - assert rows["cbp_net_table2_fy2029"]["nearest"] == "both_schemes_static_fy2029" + assert rows["centax_net_central_fy2027"]["nearest_gap"] == pytest.approx(-0.479 / -0.520 - 1) + # CBP's net gain against the dashboard's net cost: compared, signs disagree + assert rows["cbp_net_table3_fy2027"]["estimate"] == "both_schemes_dynamic_central_fy2027" + assert rows["cbp_net_table3_fy2027"]["sign_disagrees"] + assert rows["cbp_net_table3_fy2027"]["over"] # --- the gate as a property ------------------------------------------------------------- @@ -328,24 +393,33 @@ def test_childcare_central_gap_is_what_the_published_figures_imply(): money = st.floats(min_value=1e3, max_value=1e12, allow_nan=False, allow_infinity=False) -@settings(max_examples=150, deadline=None) -@given(pe=money, ext=money) -def test_gate_fails_exactly_when_an_unexplained_gap_exceeds_the_threshold(pe, ext): - pe, ext = float(round(pe)), float(round(ext)) +def _signed_record(pe: float, ext: float, annotate=False): + """A synthetic record whose external value is printed with its own sign.""" + record = synthetic(pe, ext, annotate=annotate) + return record + + +@settings(max_examples=200, deadline=None) +@given(pe=money, ext=money, pe_negative=st.booleans(), ext_negative=st.booleans()) +def test_gate_fails_exactly_when_an_unexplained_gap_exceeds_the_threshold( + pe, ext, pe_negative, ext_negative +): + pe = float(round(pe)) * (-1 if pe_negative else 1) + ext = float(round(ext)) * (-1 if ext_negative else 1) gap = pe / ext - 1 assume(abs(abs(gap) - THRESHOLD) > 1e-9) # stay off the boundary - errors = errors_of(synthetic(-pe, -ext)) + errors = errors_of(_signed_record(pe, ext)) gate_errors = [e for e in errors if "no annotation explains it" in e] assert [e for e in errors if e not in gate_errors] == [] - assert bool(gate_errors) == (abs(gap) > THRESHOLD) + should_fail = abs(gap) > THRESHOLD or (pe > 0) != (ext > 0) + assert bool(gate_errors) == should_fail @settings(max_examples=60, deadline=None) -@given(pe=money, ext=money) -def test_an_explained_gap_always_passes(pe, ext): - assert ( - errors_of(synthetic(-float(round(pe)), -float(round(ext)), annotate=True)) == [] - ) +@given(pe=money, ext=money, flip=st.booleans()) +def test_an_explained_gap_always_passes(pe, ext, flip): + ext = float(round(ext)) * (-1 if flip else 1) + assert errors_of(synthetic(-float(round(pe)), ext, annotate=True)) == [] def test_an_annotation_that_does_not_explain_the_gap_fails(): @@ -354,138 +428,200 @@ def test_an_annotation_that_does_not_explain_the_gap_fails(): assert any("explains_gap: true" in e for e in errors_of(record)) -# --- the gap rule ---------------------------------------------------------------------- +# --- no evasion: relabelling a like-for-like claim never hides its gap --------------- -nonzero = st.floats(min_value=-1e12, max_value=1e12, allow_nan=False).filter( - lambda x: abs(x) > 1e-3 -) +def _evade(fn): + """An unexplained 200% gap on a like-for-like claim, relabelled by ``fn``: + the validator must still say something.""" + record = synthetic(-300.0, -100.0) + assert any("no annotation explains it" in e for e in errors_of(record)) + fn(record) + return errors_of(record) -@given(x=nonzero) -def test_gap_is_zero_for_equal_values(x): - assert cmp.relative_gap(x, x, signs.SAME) == 0 +EVASIONS = { + "null metric": lambda r: claim0(r).update( + metric=None, proposed_metric="cost", compare_to=None, pe_missing="a different metric entirely" + ), + "uncompared with a one-line reason": lambda r: claim0(r).update( + compare_to=None, pe_missing="a different concept, says the stager" + ), + "time basis annual": lambda r: claim0(r).update(time_basis="annual"), + "period moved, fy kept": lambda r: claim0(r).update(period=2031), + "basis relabelled to net": lambda r: claim0(r)["conditions"].update(basis="post_behavioural"), + "programme relabelled": lambda r: claim0(r)["conditions"].update(program="another_programme"), + "chart reading at a different value": lambda r: claim0(r).update( + value_kind="approx_chart_reading", value=-299.0, note="read off the chart by eye" + ), + "NaN value": lambda r: claim0(r).update(value=float("nan")), + "unsigned magnitude": lambda r: claim0(r)["conditions"].update(change_direction="unsigned"), + "two-sided convention": lambda r: claim0(r)["conditions"].update( + sign_convention="positive = reduction in borrowing; negative = increase in borrowing" + ), + "cumulative": lambda r: claim0(r).update(value_kind="cumulative"), + "relabelled concept mismatch": lambda r: r.annotations.append( + note("x", ["ext_cost_fy2027"], "concept_mismatch", explains=True) + ), + "same quantity set aside as a mismatch": lambda r: ( + claim0(r).update( + compare_to=None, + pe_missing="said to be a different concept", + nearest_estimate="pe_cost_fy2027", + ), + r.annotations.append(note("x", ["ext_cost_fy2027"], "concept_mismatch")), + ), + "placeholder basis": lambda r: r.annotations.append( + {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "#"}]} + ), + "a directory as basis": lambda r: r.annotations.append( + {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "sources"}]} + ), + "a bare host as basis": lambda r: r.annotations.append( + {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "https://example.com"}]} + ), + "pending before publication": lambda r: r.annotations.append( + note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + ), + "same-day reharvest": lambda r: ( + post_publication(r, later=SEARCH_DATE), + r.annotations.append( + note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + ), + ), + "future-dated reharvest": lambda r: ( + post_publication(r, later="2099-12-31"), + r.annotations.append( + note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + ), + ), + "reharvest with no publication date": lambda r: ( + post_publication(r), + r.analysis.update(published=None), + r.annotations.append( + note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + ), + ), + "logged under open_search": lambda r: ( + claim0(r).update(found_by="open_search"), + r.search_log["searches"][0].update(producers=_producers("open_search", ["ext_cost_fy2027"])), + ), + "scale word disagrees": lambda r: claim0(r).update(value_raw="-100bn", quote="Cost -100bn"), +} -@given(pe=nonzero, ext=nonzero, k=st.floats(min_value=1e-3, max_value=1e3)) -def test_gap_is_invariant_to_rescaling_both_values(pe, ext, k): - a = cmp.relative_gap(pe, ext, signs.SAME) - b = cmp.relative_gap(pe * k, ext * k, signs.SAME) - assert math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-9) +@pytest.mark.parametrize("name", sorted(EVASIONS)) +def test_no_relabelling_hides_an_unexplained_gap(name): + errors = _evade(EVASIONS[name]) + assert errors, f"{name} let an unexplained gap through" -@given(pe=nonzero, ext=nonzero) -def test_opposite_convention_is_the_value_negated(pe, ext): - assert cmp.relative_gap(pe, -ext, signs.OPPOSITE) == cmp.relative_gap( - pe, ext, signs.SAME - ) +# --- search integrity ----------------------------------------------------------------- -@given(pe=nonzero, ext=nonzero) -def test_magnitudes_ignore_sign(pe, ext): - assert cmp.relative_gap(pe, ext, signs.MAGNITUDE) == cmp.relative_gap( - abs(pe), abs(ext), signs.SAME - ) +def test_a_search_cannot_be_dated_after_today(): + errors = mutate(lambda r: r.search_log["searches"][0].update(date="2099-01-01")) + assert any("after today" in e for e in errors) -@given(pe=nonzero, ext=nonzero) -def test_opposite_signs_are_always_over_any_threshold_below_one(pe, ext): - assume((pe > 0) != (ext > 0)) - assert cmp.relative_gap(pe, ext, signs.SAME) < -1 - assert cmp.over_threshold(cmp.relative_gap(pe, ext, signs.SAME), 0.999) +def test_a_search_cannot_predate_the_producer_list(): + def f(r): + r.search_log["searches"][0].update(date="2026-10-01", producers=[]) + claim0(r)["found_on"] = "2026-10-01" + claim0(r)["publication"]["date"] = "2026-09-01" -@given(pe=nonzero, ext=nonzero, t1=st.floats(0.01, 5), t2=st.floats(0.01, 5)) -def test_over_threshold_is_monotone_in_the_threshold(pe, ext, t1, t2): - gap = cmp.relative_gap(pe, ext, signs.SAME) - lo, hi = sorted((t1, t2)) - assert cmp.over_threshold(gap, hi) <= cmp.over_threshold(gap, lo) + errors = mutate(f) + assert any("before the UK producer list" in e for e in errors) -def test_a_published_zero(): - assert cmp.relative_gap(0.0, 0.0, signs.SAME) == 0 - assert math.isinf(cmp.relative_gap(5.0, 0.0, signs.SAME)) - assert cmp.over_threshold(cmp.relative_gap(5.0, 0.0, signs.SAME), 100) +def test_a_prepublish_search_comes_before_publication(): + errors = mutate(lambda r: r.analysis.update(published="2026-10-20")) + assert any("a prepublish search is dated on or before publication" in e for e in errors) -def test_no_rule_no_gap(): - assert cmp.relative_gap(1.0, 1.0, signs.UNKNOWN) is None - assert not cmp.over_threshold(None, 0.25) +def test_a_backfill_needs_and_follows_publication(): + def f(r): + r.search_log["searches"][0]["kind"] = "backfill" + assert any("needs analysis.json published" in e for e in mutate(f)) -# --- harvest rule 1 ------------------------------------------------------------------------ + def g(r): + r.search_log["searches"][0]["kind"] = "backfill" + r.analysis["published"] = "2026-10-22" -UNITS = {"m": "GBP m x 1e6 -> GBP", "bn": "GBP bn x 1e9 -> GBP", "": "as printed"} + errors = mutate(g) + assert any("after publication" in e or "no later than today" in e for e in errors) -@given( - cents=st.integers(min_value=0, max_value=10**9), - prefix=st.sampled_from(["", "£"]), - unit=st.sampled_from(sorted(UNITS)), - negative=st.booleans(), - minus=st.sampled_from(["-", "−"]), - grouped=st.booleans(), -) -def test_printed_values_parse_back(cents, prefix, unit, negative, minus, grouped): - number = cents / 100 - digits = f"{number:,.2f}" if grouped else f"{number:.2f}" - raw = f"{minus if negative else ''}{prefix}{digits}{unit}" - expected = -number if negative and number else number - assert cmp.parse_printed(raw) == pytest.approx(expected) - row = { - "value": expected * cmp.NORMALIZATIONS[UNITS[unit]], - "value_raw": raw, - "normalization": UNITS[unit], - } - assert cmp.check_value(row, "t") == [] - row["value"] = row["value"] * 1.01 + 1 - assert cmp.check_value(row, "t") != [] +def test_only_the_first_search_is_prepublish_or_backfill(): + def f(r): + post_publication(r, kind="reharvest") + r.search_log["searches"][1]["kind"] = "backfill" + assert any("only the first search is backfill" in e for e in mutate(f)) -@pytest.mark.parametrize( - "raw, value", - [ - ("(0.4)", -0.4), - ("−£1.2 billion", -1.2), - ("2.1 per cent", 2.1), - ("13,360", 13360), - ("+140m", 140), - ], -) -def test_printed_forms(raw, value): - assert cmp.parse_printed(raw) == value +def test_a_reharvest_find_may_wait_on_its_issue(): + record = post_publication(synthetic(-100.0, -300.0)) + assert any("no annotation explains it" in e for e in errors_of(record)) + record.annotations.append( + note( + "new-find", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/3", + ) + ) + assert errors_of(record) == [] + entry = cmp.index_entry(record, POLICY) + assert entry["pending"] == 1 + assert entry["tracking_issues"] == ["https://github.com/PolicyEngine/test/issues/3"] + report = cmp.report(record, "2026-10-22", POLICY) + assert report["new_claims"] == ["ext_cost_fy2027"] + assert report["over_threshold"][0]["tracking_issue"].endswith("/3") -def test_a_printed_range_is_two_claims(): - with pytest.raises(ValueError, match="2 numbers"): - cmp.parse_printed("£1.2bn to £1.5bn") +def test_a_backfill_find_may_wait_on_its_issue(): + def f(r): + r.analysis["published"] = "2026-10-12" + r.search_log["searches"][0]["kind"] = "backfill" -def test_a_staged_claim_is_never_re_signed(): - assert cmp.check_value( - {"value": 1, "value_raw": "1", "normalization": "as printed", "sign": -1}, "t" + record = synthetic(-100.0, -300.0) + f(record) + record.annotations.append( + note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/4") ) + assert errors_of(record) == [] -def test_an_estimate_may_be_re_signed_with_a_reason(): - row = { - "value": -434e6, - "value_raw": "0.434", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - } - assert any("sign_basis" in e for e in cmp.check_value(row, "t", signed=True)) - row["sign_basis"] = "printed as a positive cost" - assert cmp.check_value(row, "t", signed=True) == [] - +def test_a_new_results_file_needs_a_new_search(): + def f(r): + r.analysis["pe_estimates"][0]["provenance"]["commit"] = "c" * 40 -# --- each rule fails a record that breaks it ------------------------------------------------- + errors = mutate(f) + assert any("a new results file needs a new search" in e for e in errors) -def mutate(fn): +def test_a_superseded_estimate_stays_as_history(): record = synthetic(-100.0, -105.0) + old = copy.deepcopy(record.analysis["pe_estimates"][0]) + old.update(superseded_on="2026-10-21") + new = record.analysis["pe_estimates"][0] + new["provenance"]["commit"] = "c" * 40 + record.analysis["superseded_estimates"] = [old] + record.search_log["searches"][0]["results_pins"] = [ + f"PolicyEngine/test@{'c' * 40}:{SHA}" + ] assert errors_of(record) == [] - fn(record) - return errors_of(record) + + +def test_the_first_search_is_never_a_later_kind(): + for kind in cmp.LATER_KINDS: + errors = mutate(lambda r, k=kind: r.search_log["searches"][0].update(kind=k)) + assert any("prepublish or backfill" in e for e in errors), kind + + +# --- each other rule fails a record that breaks it --------------------------------- def _search(record): @@ -500,36 +636,29 @@ def test_a_skipped_producer_fails(): def test_an_unlisted_producer_fails(): errors = mutate( lambda r: _search(r).append( - { - "producer": "made_up", - "result": "none_found", - "queries": ["q"], - "claims": [], - } + {"producer": "made_up", "result": "none_found", "queries": ["q"], "claims": []} ) ) assert any("not on the UK list" in e for e in errors) def test_not_applicable_needs_the_scope_to_exclude_the_producer(): - def f(r): - _search(r)[0].update(result="not_applicable", queries=[]) - - assert any("applies to 'uk_wide'" in e for e in mutate(f)) + errors = mutate(lambda r: _search(r)[0].update(result="not_applicable", queries=[])) + assert any("applies to 'uk_wide'" in e for e in errors) def test_a_search_with_nothing_run_fails(): - def f(r): - _search(r)[0].update(queries=[], checked=[]) + errors = mutate(lambda r: _search(r)[0].update(queries=[], checked=[])) + assert any("no query run" in e for e in errors) - assert any("no query run" in e for e in mutate(f)) +def test_queries_are_a_list_of_strings(): + errors = mutate(lambda r: _search(r)[0].update(queries="x")) + assert any("lists of strings" in e for e in errors) -def test_found_not_staged_says_what_and_where(): - def f(r): - _search(r)[0].update(result="found_not_staged", note="") - errors = mutate(f) +def test_found_not_staged_says_what_and_where(): + errors = mutate(lambda r: _search(r)[0].update(result="found_not_staged", note="")) assert any("lists the pages it was seen on" in e for e in errors) assert any("says what was found" in e for e in errors) @@ -537,231 +666,349 @@ def f(r): def test_a_claim_no_search_found_fails(): def f(r): for p in _search(r): - p.update(claims=[], result="none_found") if p["producer"] == "ifs" else None + if p["producer"] == "ifs": + p.update(claims=[], result="none_found") assert any("no search entry lists this claim" in e for e in mutate(f)) def test_a_listed_producer_finds_only_its_own_claims(): - errors = mutate(lambda r: r.claims[0].update(source="resolution_foundation")) - assert any("is not ifs's" in e for e in errors) + errors = mutate(lambda r: claim0(r).update(source="resolution_foundation")) + assert any("logged under resolution_foundation's own entry" in e for e in errors) + + +@pytest.mark.parametrize("source", ["policyengine", "academic", "open_search"]) +def test_a_source_names_a_real_producer(source): + errors = mutate(lambda r: claim0(r).update(source=source)) + assert any("names the producer that published the figure" in e for e in errors) + + +def test_an_unused_primary_fails(): + def f(r): + extra = dict(r.manifest[0], sha256="d" * 64, url="https://example.org/other.pdf") + r.manifest.append(extra) + + assert any("cited by no claim" in e for e in mutate(f)) def test_a_value_that_is_not_what_was_printed_fails(): - errors = mutate(lambda r: r.claims[0].update(value=-999.0)) + errors = mutate(lambda r: claim0(r).update(value=-999.0)) assert any("is not value_raw" in e for e in errors) def test_a_quote_without_the_value_fails(): - errors = mutate(lambda r: r.claims[0].update(quote="no number here")) - assert any("quote does not contain" in e for e in errors) + errors = mutate(lambda r: claim0(r).update(quote="no number here")) + assert any("quote does not print the value" in e for e in errors) + + +def test_a_quote_must_print_the_whole_number_with_its_sign(): + assert not cmp.quote_contains("Cost 10.07", "0.07") + assert not cmp.quote_contains("Cost -800m", "800m") + assert cmp.quote_contains("Cost -800m and more", "-800m") + assert cmp.quote_contains("Abolish Cliff 0.070 0.077 0.091", "0.077") def test_an_unhashed_primary_fails(): - errors = mutate( - lambda r: r.claims[0]["publication"].update(primary_sha256="c" * 64) - ) + errors = mutate(lambda r: claim0(r)["publication"].update(primary_sha256="c" * 64)) assert any("not in manifest.jsonl" in e for e in errors) +def test_a_publication_date_is_real_and_not_after_its_finding(): + assert any("real YYYY-MM-DD" in e for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-02-31"))) + assert any("after it was found" in e for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-10-30"))) + + def test_a_different_world_needs_a_reason(): def f(r): - r.analysis["worlds"]["other"] = { - "policy": "uk_other", - "description": "another world here", - } - r.claims[0]["world"] = "other" + r.analysis["worlds"]["other"] = {"policy": "uk_other", "description": "another world here"} + claim0(r)["world"] = "other" assert any("compare across worlds only with" in e for e in mutate(f)) -def test_a_different_quantity_is_not_compared(): - errors = mutate(lambda r: r.claims[0].update(period=2029)) - assert any("period 2029 differs" in e for e in errors) +def test_a_cross_world_comparison_is_not_plainly_comparable(): + def f(r): + r.analysis["worlds"]["other"] = {"policy": "uk_other", "description": "another world here"} + claim0(r)["world"] = "other" + claim0(r)["compare_to"]["across_worlds"] = { + "reason": "the quantities are the same in both worlds, as measured", + "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/2"}], + } + r.annotations.append(note("x", ["ext_cost_fy2027"], "comparable", explains=True)) + assert any("constructed, not comparable" in e for e in mutate(f)) + + +def test_baselines_must_agree(): + errors = mutate(lambda r: claim0(r).update(baseline_policy="current_policy")) + assert any("scored against baseline current_policy" in e for e in errors) + + +def test_a_world_baseline_is_registered(): + errors = mutate(lambda r: r.analysis["worlds"]["test_reform"].update(baseline="not_a_world")) + assert any("is not registered" in e for e in errors) + + +def test_an_unregistered_claim_baseline_fails(): + errors = mutate(lambda r: claim0(r).update(baseline_policy="not_a_registered_world")) + assert any("is not registered" in e for e in errors) -def test_an_unreadable_sign_convention_fails(): - errors = mutate( - lambda r: r.claims[0]["conditions"].update( - sign_convention="negative = cost to the Exchequer" - ) - ) - assert any("does not orient the value" in e for e in errors) + +def test_a_restated_figure_is_not_compared(): + errors = mutate(lambda r: claim0(r).update(attribution="restated")) + assert any("only a producer's own estimate is compared" in e for e in errors) + + +def test_an_administrative_level_belongs_to_a_producer_source(): + errors = mutate(lambda r: claim0(r).update(benchmark_class="administrative_fact", world="baseline")) + assert any("calibration material" in e for e in errors) -def test_an_uncompared_claim_says_why(): - errors = mutate(lambda r: r.claims[0].update(compare_to=None)) - assert any("pe_missing must say why" in e for e in errors) +def test_a_compared_claim_names_no_nearest_estimate(): + errors = mutate(lambda r: claim0(r).update(nearest_estimate="pe_cost_fy2027")) + assert any("a compared claim has no nearest_estimate" in e for e in errors) -def test_a_claim_on_a_published_quantity_cannot_be_waved_off(): +def test_a_nearest_estimate_must_be_near(): def f(r): - r.claims[0].update( - compare_to=None, pe_missing="a different concept, says the stager" + claim0(r).update( + compare_to=None, + pe_missing="a different period entirely", + period=2031, + nearest_estimate="pe_cost_fy2027", ) + claim0(r)["conditions"].pop("fy") - errors = mutate(f) - assert any("matches pe_cost_fy2027 on metric, unit, period" in e for e in errors) + assert any("is not near this claim" in e for e in mutate(f)) -def test_a_waved_off_claim_needs_a_cited_mismatch(): +def test_a_claim_beside_its_nearest_estimate_needs_a_cited_mismatch(): def name_it(r): - r.claims[0].update( + claim0(r).update( compare_to=None, pe_missing="the producer's figure is net of behaviour", nearest_estimate="pe_cost_fy2027", ) + claim0(r)["conditions"]["basis"] = "post_behavioural" assert any("must say why" in e for e in mutate(name_it)) def annotate(r): name_it(r) - r.annotations.append( - { - "id": "net-vs-static", - "claims": [r.claims[0]["key"]], - "status": "concept_mismatch", - "explains_gap": False, - "cause": "the producer's figure is net of behavioural responses", - "basis": [ - {"kind": "document", "ref": "https://example.org/report.pdf#page=3"} - ], - } - ) + r.annotations.append(note("net-vs-static", ["ext_cost_fy2027"], "concept_mismatch")) assert mutate(annotate) == [] - def comparable_status(r): - annotate(r) - r.annotations[0]["status"] = "comparable" - assert any("must say why" in e for e in mutate(comparable_status)) +def test_a_range_needs_both_bounds_and_is_judged_as_a_range(): + def lone(r): + claim0(r).update(value_kind="range_low") + claim0(r)["conditions"]["range_group"] = "g" + + assert any("needs exactly one range_low and one range_high" in e for e in mutate(lone)) + + def pair(pe, low, high): + record = synthetic(pe, low) + c = claim0(record) + c.update(value_kind="range_low") + c["conditions"]["range_group"] = "g" + other = copy.deepcopy(c) + printed = ("-" if high < 0 else "") + repr(abs(high)) + other.update(key="ext_cost_high_fy2027", value_kind="range_high", value=high, value_raw=printed, quote=f"Cost {printed}") + record.claims.append(other) + for p in _search(record): + if p["producer"] == "ifs": + p["claims"].append(other["key"]) + return record + + inside = pair(-150.0, -100.0, -200.0) + assert errors_of(inside) == [] + assert not any(r["over"] for r in cmp.evaluate(inside, POLICY)) + outside = pair(-300.0, -100.0, -200.0) + rows = cmp.evaluate(outside, POLICY) + assert all(r["over"] for r in rows) + assert rows[0]["group_gap"] == pytest.approx(300 / 200 - 1) -def test_nearest_estimate_must_match(): +def test_an_issue_basis_is_a_github_issue(): def f(r): - r.claims[0].update( - compare_to=None, - pe_missing="a different period entirely", - period=2031, - nearest_estimate="pe_cost_fy2027", + r.annotations.append( + {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "issue", "ref": "https://example.org/ticket/1"}]} ) - assert any("does not match this claim" in e for e in mutate(f)) - - -def test_a_restated_figure_is_not_compared(): - errors = mutate(lambda r: r.claims[0].update(attribution="restated")) - assert any("only a producer's own estimate is compared" in e for e in errors) - + assert any("GitHub issue or pull URL" in e for e in mutate(f)) -def test_an_unregistered_baseline_fails(): - errors = mutate( - lambda r: r.claims[0].update(baseline_policy="not_a_registered_world") - ) - assert any("is not registered" in e for e in errors) +def test_a_basis_may_be_a_file_in_this_repository(): + assert cmp._check_basis({"kind": "document", "ref": "docs/COMPARABLES.md#the-gap"}, "t") == [] + assert cmp._check_basis({"kind": "document", "ref": "docs"}, "t") != [] + assert cmp._check_basis({"kind": "document", "ref": "../../etc/passwd"}, "t") != [] -def test_an_administrative_level_belongs_to_a_producer_source(): - def f(r): - r.claims[0].update(benchmark_class="administrative_fact", world="baseline") - assert any("calibration material" in e for e in mutate(f)) +def test_a_staged_claim_is_never_re_signed(): + assert cmp.check_value({"value": 1, "value_raw": "1", "normalization": "as printed", "sign": -1}, "t") -def test_an_annotation_without_a_basis_fails(): - def f(r): - r.annotations.append( - { - "id": "x", - "claims": [r.claims[0]["key"]], - "status": "comparable", - "explains_gap": True, - "cause": "a cause long enough to count", - "basis": [], - } - ) +def test_an_estimate_may_be_re_signed_with_a_reason(): + row = {"value": -434e6, "value_raw": "0.434", "normalization": "GBP bn x 1e9 -> GBP", "sign": -1} + assert any("sign_basis" in e for e in cmp.check_value(row, "t", signed=True)) + row["sign_basis"] = "printed as a positive cost" + assert cmp.check_value(row, "t", signed=True) == [] + row["sign"] = True + assert any("integer 1 or -1" in e for e in cmp.check_value(row, "t", signed=True)) - assert any("no fabricated mechanisms" in e for e in mutate(f)) +def test_duplicate_json_keys_fail(tmp_path): + directory = tmp_path / "analysis-uk-dup" + directory.mkdir() + for name in cmp.FILES: + (directory / name).write_text("[]" if name.endswith(".json") else "") + (directory / "analysis.json").write_text('{"a": 1, "a": 2}') + record = cmp.load_analysis(directory) + assert any("duplicate key" in e for e in record.load_errors) -def test_an_issue_basis_is_a_github_issue(): - def f(r): - r.annotations.append( - { - "id": "x", - "claims": [r.claims[0]["key"]], - "status": "comparable", - "explains_gap": True, - "cause": "a cause long enough to count", - "basis": [{"kind": "issue", "ref": "https://example.org/ticket/1"}], - } - ) - assert any("GitHub issue or pull URL" in e for e in mutate(f)) +# --- the gap rule ---------------------------------------------------------------------- +nonzero = st.floats(min_value=-1e12, max_value=1e12, allow_nan=False).filter(lambda x: abs(x) > 1e-3) -def test_pending_is_only_for_what_a_reharvest_found(): - def f(r): - r.annotations.append( - { - "id": "x", - "claims": [r.claims[0]["key"]], - "status": "pending", - "explains_gap": False, - "cause": "found after publication; being explained", - "basis": [ - { - "kind": "issue", - "ref": "https://github.com/PolicyEngine/test/issues/2", - } - ], - "tracking_issue": "https://github.com/PolicyEngine/test/issues/2", - } - ) - assert any("explained before the analysis is recorded" in e for e in mutate(f)) +@given(x=nonzero) +def test_gap_is_zero_for_equal_values(x): + assert cmp.relative_gap(x, x, signs.SAME) == 0 -def test_a_reharvest_find_may_wait_on_its_issue(): - record = synthetic(-100.0, -300.0) - first = copy.deepcopy(record.search_log["searches"][0]) - for p in first["producers"]: - if p["producer"] == "ifs": - p.update(result="none_found", claims=[]) - later = record.search_log["searches"][0] - later.update(date="2026-11-02", kind="reharvest") - record.search_log["searches"] = [first, later] - record.claims[0]["found_on"] = "2026-11-02" - assert any("no annotation explains it" in e for e in errors_of(record)) - record.annotations.append( - { - "id": "new-find", - "claims": [record.claims[0]["key"]], - "status": "pending", - "explains_gap": False, - "cause": "found by the weekly re-harvest; the owner is explaining it", - "basis": [ - { - "kind": "issue", - "ref": "https://github.com/PolicyEngine/test/issues/3", - } - ], - "tracking_issue": "https://github.com/PolicyEngine/test/issues/3", - } - ) - assert errors_of(record) == [] - entry = cmp.index_entry(record, POLICY) - assert entry["pending"] == 1 - assert entry["tracking_issues"] == ["https://github.com/PolicyEngine/test/issues/3"] - report = cmp.report(record, "2026-11-01", POLICY) - assert report["new_claims"] == ["ext_cost_fy2027"] - assert report["over_threshold"][0]["tracking_issue"].endswith("/3") +@given(pe=nonzero, ext=nonzero, k=st.floats(min_value=1e-3, max_value=1e3)) +def test_gap_is_invariant_to_rescaling_both_values(pe, ext, k): + a = cmp.relative_gap(pe, ext, signs.SAME) + b = cmp.relative_gap(pe * k, ext * k, signs.SAME) + assert math.isclose(a, b, rel_tol=1e-9, abs_tol=1e-9) + + +@given(pe=nonzero, ext=nonzero) +def test_opposite_convention_is_the_value_negated(pe, ext): + assert cmp.relative_gap(pe, -ext, signs.OPPOSITE) == cmp.relative_gap(pe, ext, signs.SAME) + + +@given(pe=nonzero, ext=nonzero) +def test_magnitudes_ignore_sign(pe, ext): + assert cmp.relative_gap(pe, ext, signs.MAGNITUDE) == cmp.relative_gap(abs(pe), abs(ext), signs.SAME) + + +@given(pe=nonzero, ext=nonzero, t=st.floats(0.001, 0.999)) +def test_a_sign_disagreement_is_over_every_allowed_threshold(pe, ext, t): + assume((pe > 0) != (ext > 0)) + assert cmp.sign_disagrees(pe, ext) + assert cmp.over_threshold(cmp.relative_gap(pe, ext, signs.SAME), t) + + +@given(pe=nonzero, ext=nonzero, t1=st.floats(0.01, 0.99), t2=st.floats(0.01, 0.99)) +def test_over_threshold_is_monotone_in_the_threshold(pe, ext, t1, t2): + gap = cmp.relative_gap(pe, ext, signs.SAME) + lo, hi = sorted((t1, t2)) + assert cmp.over_threshold(gap, hi) <= cmp.over_threshold(gap, lo) + + +def test_a_published_zero(): + assert cmp.relative_gap(0.0, 0.0, signs.SAME) == 0 + assert math.isinf(cmp.relative_gap(5.0, 0.0, signs.SAME)) + assert cmp.over_threshold(cmp.relative_gap(5.0, 0.0, signs.SAME), 0.99) + + +def test_no_rule_no_gap(): + assert cmp.relative_gap(1.0, 1.0, signs.UNKNOWN) is None + assert not cmp.over_threshold(None, 0.25) + + +@pytest.mark.parametrize( + "convention", + [ + "positive = reduction in borrowing; negative = increase in borrowing", + "negative = cost to the Exchequer", + "positive = yield forgone", + "", + ], +) +def test_ambiguous_conventions_do_not_orient(convention): + claim = {"metric": "revenue_change", "conditions": {"sign_convention": convention}, "source": "x"} + assert cmp.orientation(claim, "UK")[0] == signs.UNKNOWN + + +def test_the_policy_keeps_the_threshold_below_one(tmp_path): + for bad in (1, 1.5, 0, True): + path = tmp_path / "policy.json" + path.write_text(json.dumps(dict(POLICY, gap_threshold=bad))) + with pytest.raises(ValueError, match="between 0 and 1"): + cmp.load_policy(path) + + +def test_the_policy_rejects_a_bad_window(tmp_path): + path = tmp_path / "policy.json" + path.write_text(json.dumps(dict(POLICY, promotion_window_days=0))) + with pytest.raises(ValueError, match="promotion_window_days"): + cmp.load_policy(path) + + +# --- harvest rule 1 ------------------------------------------------------------------------ + +UNITS = {"m": "GBP m x 1e6 -> GBP", "bn": "GBP bn x 1e9 -> GBP", "": "as printed"} -def test_the_first_search_is_never_a_reharvest(): - errors = mutate(lambda r: r.search_log["searches"][0].update(kind="reharvest")) - assert any("prepublish or backfill" in e for e in errors) +@given( + cents=st.integers(min_value=0, max_value=10**9), + prefix=st.sampled_from(["", "£"]), + unit=st.sampled_from(sorted(UNITS)), + negative=st.booleans(), + minus=st.sampled_from(["-", "−", "‒", "-"]), + grouped=st.booleans(), +) +def test_printed_values_parse_back(cents, prefix, unit, negative, minus, grouped): + number = cents / 100 + digits = f"{number:,.2f}" if grouped else f"{number:.2f}" + raw = f"{minus if negative else ''}{prefix}{digits}{unit}" + expected = -number if negative and number else number + assert cmp.parse_printed(raw) == pytest.approx(expected) + row = { + "value": expected * cmp.NORMALIZATIONS[UNITS[unit]][0], + "value_raw": raw, + "normalization": UNITS[unit], + } + assert cmp.check_value(row, "t") == [] + row["value"] = row["value"] * 1.01 + 1 + assert cmp.check_value(row, "t") != [] + + +@pytest.mark.parametrize( + "raw, value", + [ + ("(0.4)", -0.4), + ("£(0.4)bn", -0.4), + ("(£0.4bn)", -0.4), + ("−£1.2 billion", -1.2), + ("2.1 per cent", 2.1), + ("13,360", 13360), + ("+140m", 140), + ("post-tax £0.8bn", 0.8), + ("minus £3m", -3), + ], +) +def test_printed_forms(raw, value): + assert cmp.parse_printed(raw) == value + + +@pytest.mark.parametrize( + "raw, normalization", + [("£800m", "GBP bn x 1e9 -> GBP"), ("2.1%", "GBP bn x 1e9 -> GBP"), ("£0.8bn", "as printed")], +) +def test_a_printed_scale_must_match_the_normalization(raw, normalization): + value = cmp.parse_printed(raw) * cmp.NORMALIZATIONS[normalization][0] + errors = cmp.check_value({"value": value, "value_raw": raw, "normalization": normalization}, "t") + assert any("states the scale" in e for e in errors) + + +def test_a_printed_range_is_two_claims(): + with pytest.raises(ValueError, match="2 numbers"): + cmp.parse_printed("£1.2bn to £1.5bn") # --- the app-v2 record ------------------------------------------------------------------------ @@ -772,65 +1019,173 @@ def test_app_record_and_index_cite_the_same_search(directory): record = cmp.load_analysis(directory) block = cmp.app_record(record) entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] - search = next(s for s in entry["searches"] if s["date"] == block["searchDate"]) + search = next(s for s in entry["searches"] if s["date"] == block["searchDate"] and s["kind"] == block["searchKind"]) assert block["scorecardSource"] == directory.name assert block["producersSearched"] == search["producers_searched"] assert block["found"] == search["found"] - assert block["searchKind"] == search["kind"] assert block["searchLog"] == entry["search_log"] +@pytest.mark.parametrize("directory", cmp.analysis_dirs(), ids=lambda d: d.name) +def test_index_comparisons_are_every_staged_estimate(directory): + record = cmp.load_analysis(directory) + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] + rows = {r["key"]: r for r in cmp.evaluate(record, POLICY)} + assert [c["claim"] for c in entry["comparisons"]] == [c["key"] for c in record.claims] + assert entry["current_pins"] == cmp.current_pins(record.analysis) + for c in entry["comparisons"]: + row = rows[c["claim"]] + assert c["estimate"] == row["estimate"] + if row["gap"] is not None and not math.isinf(row["gap"]): + assert c["gap"] == pytest.approx(row["gap"], abs=5e-5) + assert (c["pe_value"] is None) == (row["estimate"] is None) + assert c["nearest_estimate"] == row["nearest"] + assert (c["nearest_pe_value"] is None) == (row["nearest"] is None) + if row["over"]: + assert c["cause"], f"{c['claim']} is over the threshold with no cause" + + def test_scaffold_lists_every_producer(tmp_path): directory = cmp.scaffold("uk", "app", "new-dashboard", SEARCH_DATE, root=tmp_path) log = json.loads((directory / "search_log.json").read_text()) logged = [p["producer"] for p in log["searches"][0]["producers"]] - assert logged == [ - p["slug"] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE) - ] + assert logged == [p["slug"] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE)] assert directory.name == "analysis-uk-new-dashboard" +def test_scaffold_refuses_a_country_with_no_list(tmp_path): + with pytest.raises(SystemExit, match="no producer list for NG"): + cmp.scaffold("ng", "app", "x", SEARCH_DATE, root=tmp_path) + + +def test_post_slug_is_the_websites(): + assert cmp.post_slug("UK_Two_Child-limit.md") == "uk-two-child-limit" + assert cmp.post_slug("a.b.md") == "a" + + +def test_no_estimate_is_only_for_posts_that_are_not_analyses(): + assert cmp.may_declare_no_estimate("post", ["us", "giving"]) + assert not cmp.may_declare_no_estimate("post", ["uk", "autumn-budget", "featured"]) + assert not cmp.may_declare_no_estimate("post", ["us", "giving", "policy"]) + assert not cmp.may_declare_no_estimate("app", ["us", "giving"]) + + def test_due_lists_recent_estimates_only(tmp_path): apps = [ - { - "slug": "a", - "countryId": "uk", - "date": "2026-10-12 12:00:00", - "displayWithResearch": True, - "tags": ["uk", "policy"], - }, - { - "slug": "old", - "countryId": "uk", - "date": "2026-01-01", - "displayWithResearch": True, - "tags": ["uk", "policy"], - }, - { - "slug": "tool", - "countryId": "us", - "date": "2026-10-01", - "displayWithResearch": True, - "tags": ["us"], - }, - {"slug": "unlisted", "countryId": "us", "tags": ["us", "policy"]}, + {"slug": "a", "countryId": "uk", "date": "2026-10-12 12:00:00", "displayWithResearch": True, "tags": ["uk", "policy"]}, + {"slug": "old", "countryId": "uk", "date": "2026-01-01", "displayWithResearch": True, "tags": ["uk", "policy"]}, + {"slug": "tool", "countryId": "us", "date": "2026-10-01", "displayWithResearch": True, "tags": ["us"]}, + {"slug": "unlisted", "countryId": "us", "date": "2026-10-03", "tags": ["us", "policy"]}, ] posts = [ {"filename": "Some_Post.md", "date": "2026-10-02", "tags": ["us", "policy"]}, - { - "filename": "event.md", - "date": "2026-10-03", - "tags": ["uk", "event"], - "comparables": {"status": "no_estimate", "reason": "x"}, - }, + {"filename": "budget-costing.md", "date": "2026-10-04", "tags": ["uk", "autumn-budget"]}, + {"filename": "event.md", "date": "2026-10-03", "tags": ["uk", "event"], "comparables": {"status": "no_estimate", "reason": "x"}}, + {"filename": "untagged.md", "date": "2026-10-05", "tags": ["technical"]}, ] rows = cmp.due(apps, posts, "2026-10-21", 183, root=tmp_path) assert [(r["kind"], r["source"]) for r in rows] == [ ("post", "analysis-us-some-post"), + ("app", "analysis-us-unlisted"), + ("post", "analysis-uk-budget-costing"), + ("post", "analysis-us-untagged"), ("app", "analysis-uk-a"), ] +def test_due_refuses_two_entries_sharing_a_record(tmp_path): + apps = [{"slug": "x", "countryId": "uk", "date": "2026-10-12", "displayWithResearch": True, "tags": ["uk", "policy"]}] + posts = [{"filename": "x.md", "date": "2026-10-13", "tags": ["uk", "policy"]}] + with pytest.raises(ValueError, match="would share the record analysis-uk-x"): + cmp.due(apps, posts, "2026-10-21", 183, root=tmp_path) + + +# --- promotion: a fresh search before a push ---------------------------------------- + + +def _entry(search_dates, pending=(), pins_match=True): + return { + "current_pins": [PIN], + "searches": [ + {"date": d, "kind": "reharvest", "results_pins": [PIN] if pins_match else ["x"]} + for d in search_dates + ], + "comparisons": [ + {"claim": f"c{i}", "status": "pending", "found_on": found} + for i, found in enumerate(pending) + ], + } + + +days_st = st.integers(min_value=-400, max_value=400) +PUSH = "2026-11-02" + + +@given(offsets=st.lists(days_st, min_size=1, max_size=6), window=st.integers(1, 60)) +def test_fresh_exactly_when_a_search_falls_in_the_window(offsets, window): + dates = sorted(shift(PUSH, o) for o in offsets) + in_window = any(-window <= o <= 0 for o in offsets) + assert (cmp.fresh(_entry(dates), PUSH, window) == []) == in_window + + +@given(offsets=st.lists(days_st, min_size=1, max_size=6), w1=st.integers(1, 60), w2=st.integers(1, 60)) +def test_a_wider_window_is_never_stricter(offsets, w1, w2): + dates = sorted(shift(PUSH, o) for o in offsets) + narrow, wide = sorted((w1, w2)) + if cmp.fresh(_entry(dates), PUSH, narrow) == []: + assert cmp.fresh(_entry(dates), PUSH, wide) == [] + + +@given(found=days_st) +def test_a_pending_gap_blocks_a_push_only_once_found(found): + reasons = cmp.fresh(_entry([PUSH], pending=[shift(PUSH, found)]), PUSH, WINDOW) + assert bool(reasons) == (found <= 0) + + +def test_a_search_against_old_results_is_not_fresh(): + reasons = cmp.fresh(_entry([PUSH], pins_match=False), PUSH, WINDOW) + assert reasons and "compared the results the page reads now" in reasons[0] + + +def test_a_search_after_the_push_does_not_count(): + assert cmp.fresh(_entry([shift(PUSH, 1)]), PUSH, WINDOW) != [] + + +def test_the_childcare_record_is_fresh_only_within_its_window(): + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][CHILDCARE.name] + assert cmp.fresh(entry, "2026-10-21", WINDOW) == [] + assert cmp.fresh(entry, shift("2026-10-21", WINDOW), WINDOW) == [] + late = cmp.fresh(entry, shift("2026-10-21", WINDOW + 1), WINDOW) + assert late and "run a prepromotion search" in late[0] + + +def _fresh_cases() -> list[dict]: + """The cases PolicyEngine/newsletters' fresh is checked against too.""" + cases = [] + for window in (3, 7): + for search in (-9, -8, -7, -3, 0, 1): + for pins_match in (True, False): + for pending in (None, -2, 0, 2): + entry = _entry( + [shift(PUSH, search)], + pending=() if pending is None else [shift(PUSH, pending)], + pins_match=pins_match, + ) + cases.append( + { + "entry": entry, + "on": PUSH, + "window": window, + "fresh": cmp.fresh(entry, PUSH, window) == [], + } + ) + return cases + + +def test_the_shared_fresh_cases_are_current(): + assert json.loads(FRESH_CASES.read_text()) == _fresh_cases() + + # --- the database --------------------------------------------------------------------------- @@ -857,182 +1212,49 @@ def test_every_analysis_claim_is_held_out(db): assert {r["calibration_relationship"] for r in rows} == {"held_out"} -def test_db_matches_the_records(db): +def test_db_holds_exactly_the_records_claims(db): expected = {} for directory in cmp.analysis_dirs(): record = cmp.load_analysis(directory) + claims = {c["key"]: c for c in record.claims} for row in cmp.evaluate(record, POLICY): - expected[(record.analysis["id"], row["key"])] = row - rows = _analysis_claims(db) + c = claims[row["key"]] + if c["attribution"] == "restated" or c.get("metric") is None or c.get("unit_concept") is None: + continue + expected[(record.analysis["id"], row["key"])] = (row, c) seen = set() - for r in rows: + for r in _analysis_claims(db): cond = json.loads(r["conditions"]) key = (cond["analysis"], cond["analysis_claim"]) seen.add(key) + row, claim = expected[key] results = db.execute( - "SELECT status, computed_value FROM pe_results WHERE claim_id = ?", - (r["claim_id"],), + "SELECT status, computed_value FROM pe_results WHERE claim_id = ?", (r["claim_id"],) ).fetchall() - if expected[key]["estimate"]: - assert len(results) == 1 - want = expected[key]["status"] - assert results[0]["status"] == ("comparable" if want == "pending" else want) - elif expected[key]["nearest"]: - # beside its nearest estimate, labelled with the stated mismatch - assert len(results) == 1 - assert results[0]["status"] == expected[key]["status"] - assert results[0]["status"] in cmp.MISMATCH_STATUSES - assert cond["pe_missing"] + history = 1 + sum( + 1 + for e in cmp.load_analysis(cmp.SOURCES / f"analysis-{key[0].replace('/', '-')}").analysis.get("superseded_estimates", []) + if e["key"] in (row["estimate"], row["nearest"]) + ) + if row["estimate"] or row["nearest"]: + assert len(results) == history + want = row["status"] + if want == "pending": + want = "comparable" + assert {res["status"] for res in results} == {want} or want == "comparable" else: assert results == [] assert cond["pe_missing"] - staged = {k for k, row in expected.items()} - assert seen <= staged + assert seen == set(expected) def test_childcare_result_is_the_published_figure(db): row = db.execute( - "SELECT r.computed_value, r.run_id, s.value FROM external_scores s JOIN pe_results r" + "SELECT r.computed_value, r.run_id, r.status, s.value FROM external_scores s JOIN pe_results r" " ON r.claim_id = s.claim_id WHERE json_extract(s.conditions, '$.analysis_claim') = ?", ("centax_static_central_fy2027",), ).fetchone() assert row["computed_value"] == -434e6 assert row["value"] == -800e6 + assert row["status"] == "constructed" assert row["run_id"] == "analysis:uk/childcare-100k-threshold@f25f0db" - - -@pytest.mark.parametrize("directory", cmp.analysis_dirs(), ids=lambda d: d.name) -def test_index_comparisons_are_every_staged_estimate(directory): - record = cmp.load_analysis(directory) - entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] - rows = {r["key"]: r for r in cmp.evaluate(record, POLICY)} - assert [c["claim"] for c in entry["comparisons"]] == [ - c["key"] for c in record.claims - ] - for c in entry["comparisons"]: - row = rows[c["claim"]] - assert c["estimate"] == row["estimate"] - if row["gap"] is not None and not math.isinf(row["gap"]): - assert c["gap"] == pytest.approx(row["gap"], abs=5e-5) - assert (c["pe_value"] is None) == (row["estimate"] is None) - assert c["nearest_estimate"] == row["nearest"] - assert (c["nearest_pe_value"] is None) == (row["nearest"] is None) - assert (c["pe_missing"] is None) == (row["estimate"] is not None) - if row["over"]: - assert c["cause"], f"{c['claim']} is over the threshold with no cause" - - -# --- promotion: a fresh search before a push ---------------------------------------- - -WINDOW = POLICY["promotion_window_days"] - - -def _entry(search_dates, pending=()): - return { - "searches": [{"date": d, "kind": "reharvest"} for d in search_dates], - "comparisons": [ - {"claim": f"c{i}", "status": "pending", "found_on": found} - for i, found in enumerate(pending) - ], - } - - -days_st = st.integers(min_value=-400, max_value=400) - - -def _shift(days): - import datetime as dt - - return (dt.date(2026, 11, 2) + dt.timedelta(days=days)).isoformat() - - -@given(offsets=st.lists(days_st, min_size=1, max_size=6), window=st.integers(1, 60)) -def test_fresh_exactly_when_a_search_falls_in_the_window(offsets, window): - on = _shift(0) - dates = sorted(_shift(o) for o in offsets) - in_window = any(-window <= o <= 0 for o in offsets) - assert (cmp.fresh(_entry(dates), on, window) == []) == in_window - - -@given( - offsets=st.lists(days_st, min_size=1, max_size=6), - w1=st.integers(1, 60), - w2=st.integers(1, 60), -) -def test_a_wider_window_is_never_stricter(offsets, w1, w2): - on = _shift(0) - dates = sorted(_shift(o) for o in offsets) - narrow, wide = sorted((w1, w2)) - if cmp.fresh(_entry(dates), on, narrow) == []: - assert cmp.fresh(_entry(dates), on, wide) == [] - - -@given(found=days_st) -def test_a_pending_gap_blocks_a_push_only_once_found(found): - on = _shift(0) - reasons = cmp.fresh(_entry([on], pending=[_shift(found)]), on, WINDOW) - assert bool(reasons) == (found <= 0) - - -def test_a_search_after_the_push_does_not_count(): - assert cmp.fresh(_entry([_shift(1)]), _shift(0), WINDOW) != [] - - -def test_the_childcare_record_was_fresh_on_its_search_day_only_for_the_window(): - entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][CHILDCARE.name] - assert cmp.fresh(entry, "2026-10-21", WINDOW) == [] - assert cmp.fresh(entry, f"2026-10-{21 + WINDOW:02d}", WINDOW) == [] - late = cmp.fresh(entry, f"2026-10-{22 + WINDOW:02d}", WINDOW) - assert late and "run a prepromotion search" in late[0] - - -def test_the_index_carries_the_window_and_when_each_claim_was_found(): - index = json.loads(cmp.INDEX_PATH.read_text()) - assert index["promotion_window_days"] == WINDOW - for entry in index["analyses"].values(): - assert all("found_on" in c for c in entry["comparisons"]) - - -def test_a_prepromotion_search_is_never_the_first(): - errors = mutate(lambda r: r.search_log["searches"][0].update(kind="prepromotion")) - assert any("prepublish or backfill" in e for e in errors) - - -def test_a_prepromotion_find_may_wait_on_its_issue_but_blocks_the_push(): - record = synthetic(-100.0, -300.0) - first = copy.deepcopy(record.search_log["searches"][0]) - for p in first["producers"]: - if p["producer"] == "ifs": - p.update(result="none_found", claims=[]) - later = record.search_log["searches"][0] - later.update(date="2026-11-02", kind="prepromotion") - record.search_log["searches"] = [first, later] - record.claims[0]["found_on"] = "2026-11-02" - record.annotations.append( - { - "id": "found-before-a-push", - "claims": [record.claims[0]["key"]], - "status": "pending", - "explains_gap": False, - "cause": "found by the search before the newsletter; the owner is explaining it", - "basis": [ - { - "kind": "issue", - "ref": "https://github.com/PolicyEngine/test/issues/4", - } - ], - "tracking_issue": "https://github.com/PolicyEngine/test/issues/4", - } - ) - assert errors_of(record) == [] - entry = cmp.index_entry(record, POLICY) - reasons = cmp.fresh(entry, "2026-11-03", WINDOW) - assert len(reasons) == 1 and "pending" in reasons[0] - - -def test_the_policy_rejects_a_bad_window(tmp_path): - bad = dict(POLICY, promotion_window_days=0) - path = tmp_path / "policy.json" - path.write_text(json.dumps(bad)) - with pytest.raises(ValueError, match="promotion_window_days"): - cmp.load_policy(path) From 3aeed8ae9922c9d333e603f9c5eaaccf2f19aacb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 11:47:02 -0400 Subject: [PATCH 07/15] Close the review's holes in the comparables gate The validator now checks each quantity's full identity (basis, programme, fiscal year), search kinds and dates, the results files each search compared, ranges, scales, quotes, sign conventions and bases, and refuses every relabelling the review used to hide a gap. The childcare record is regenerated under the new rules; the docs say what is enforced where. Co-Authored-By: Claude Opus 5.5 --- README.md | 13 +- data/comparables/index.json | 480 +++- docs/ARCHITECTURE.md | 15 +- docs/COMPARABLES.md | 195 +- .../README.md | 25 +- tests/fixtures/comparables_fresh_cases.json | 2354 +++++++++++++++++ 6 files changed, 2885 insertions(+), 197 deletions(-) create mode 100644 tests/fixtures/comparables_fresh_cases.json diff --git a/README.md b/README.md index 08ec3aa3d..6fe74c379 100644 --- a/README.md +++ b/README.md @@ -78,11 +78,14 @@ python pipeline/build_moments.py # -> app/public/data/moments.js ## Pre-publish comparables -Every PolicyEngine analysis is searched against every external producer -before it is published, and each estimate found is recorded here with the -cause of any gap over 25% (docs/COMPARABLES.md). One directory per -analysis, `sources/analysis--/`; the validator and index are -`scorecard_db/comparables.py`, and policyengine-app-v2's CI reads the index. +A PolicyEngine analysis is searched against every listed external producer +before it is published or promoted, and each estimate found is recorded here +with the cause of any gap over the threshold, 25% by default +(docs/COMPARABLES.md). One directory per analysis, +`sources/analysis--/`; the validator and index are +`scorecard_db/comparables.py`. policyengine-app-v2's CI +(PolicyEngine/policyengine-app-v2#1380) and PolicyEngine/newsletters' CI +(PolicyEngine/newsletters#29) read the index. ## Reproducing diff --git a/data/comparables/index.json b/data/comparables/index.json index 26fc02d4b..cbc119e94 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -1,5 +1,5 @@ { - "_what": "Every PolicyEngine analysis whose comparables search is recorded in this repository, keyed by its sources/ directory. Generated by `python -m scorecard_db.comparables index` from the directories themselves; policyengine-app-v2 reads it to check a research entry's comparables record. A listing means the record passed the gate.", + "_what": "Every PolicyEngine analysis whose comparables search is recorded in this repository, keyed by its sources/ directory. Generated by `python -m scorecard_db.comparables index` from the directories themselves; policyengine-app-v2 and PolicyEngine/newsletters read it to check an entry's comparables. A listing means the record passed the gate.", "schema": "comparables/index/1", "gap_threshold": 0.25, "promotion_window_days": 7, @@ -13,10 +13,16 @@ "url": "https://policyengine.org/uk/childcare-100k-threshold", "owner": "vahid-ahmadi", "published": "2026-10-12", + "current_pins": [ + "PolicyEngine/uk-childcare-100k-threshold@f25f0dbd46c31d336bde06898d801a1887a8cf89:4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a" + ], "searches": [ { "date": "2026-10-21", "kind": "backfill", + "results_pins": [ + "PolicyEngine/uk-childcare-100k-threshold@f25f0dbd46c31d336bde06898d801a1887a8cf89:4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a" + ], "producers_searched": [ "academic", "centax", @@ -49,16 +55,18 @@ "not_applicable": [ "scottish_fiscal_commission" ], - "found": 20 + "found": 22 } ], - "claims": 20, - "compared": 6, - "over_threshold": 6, - "explained": 6, + "claims": 22, + "compared": 15, + "beside_nearest": 7, + "over_threshold": 15, + "sign_disagreements": 8, + "explained": 15, "pending": 0, "unbounded_gaps": 0, - "max_abs_gap": 0.5, + "max_abs_gap": 7.4429, "tracking_issues": [], "search_log": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/sources/analysis-uk-childcare-100k-threshold/search_log.json", "comparisons": [ @@ -71,8 +79,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "central", + "range_group": null, "value": -800000000, "value_raw": "-800m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -80,10 +92,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.4575, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -95,8 +112,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "central", + "range_group": null, "value": -520000000, "value_raw": "-520m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -104,11 +125,16 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2027", - "nearest_pe_value": -434000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -0.0788, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "centax_static_central_fy2029", @@ -119,8 +145,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "central", + "range_group": null, "value": -980000000, "value_raw": "-980m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -128,10 +158,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.4847, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -143,8 +178,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "central", + "range_group": null, "value": -640000000, "value_raw": "-640m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -152,11 +191,16 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2029", - "nearest_pe_value": -505000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2029", + "nearest_pe_value": -556000000, + "nearest_gap": -0.1312, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "centax_static_high_cost_fy2027", @@ -167,8 +211,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "range_low", + "range_group": "centax_static_fy2027_scenarios", "value": -820000000, "value_raw": "-820m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -176,10 +224,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.4707, + "gap_unbounded": false, + "group_gap": -0.2517, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -191,8 +244,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "range_low", + "range_group": "centax_net_fy2027_scenarios", "value": -610000000, "value_raw": "-610m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -200,11 +257,16 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2027", - "nearest_pe_value": -434000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -0.2148, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "centax_static_high_cost_fy2029", @@ -215,8 +277,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "range_low", + "range_group": "centax_static_fy2029_scenarios", "value": -1010000000, "value_raw": "-1,010m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -224,10 +290,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.5, + "gap_unbounded": false, + "group_gap": -0.2786, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -239,8 +310,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "range_low", + "range_group": "centax_net_fy2029_scenarios", "value": -770000000, "value_raw": "-770m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -248,11 +323,16 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2029", - "nearest_pe_value": -505000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2029", + "nearest_pe_value": -556000000, + "nearest_gap": -0.2779, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "centax_static_low_cost_fy2027", @@ -263,8 +343,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "range_high", + "range_group": "centax_static_fy2027_scenarios", "value": -580000000, "value_raw": "-580m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -272,10 +356,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2027-28", "pe_value": -434000000, "gap": -0.2517, + "gap_unbounded": false, + "group_gap": -0.2517, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -287,8 +376,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2028, "fy": "2027-28", + "value_kind": "range_high", + "range_group": "centax_net_fy2027_scenarios", "value": 140000000, "value_raw": "+140m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -296,11 +389,16 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2027", - "nearest_pe_value": -434000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -4.4214, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "centax_static_low_cost_fy2029", @@ -311,8 +409,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "range_high", + "range_group": "centax_static_fy2029_scenarios", "value": -700000000, "value_raw": "-700m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -320,10 +422,15 @@ "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2029-30", "pe_value": -505000000, "gap": -0.2786, + "gap_unbounded": false, + "group_gap": -0.2786, + "sign_disagrees": false, + "over_threshold": true, "nearest_estimate": null, "nearest_pe_value": null, + "nearest_gap": null, "status": "constructed", - "cause": "The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives £1.009bn and £1.215bn for the 30 hours, 26% and 24% above CenTax's central static cost.", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", "pe_missing": null }, { @@ -335,8 +442,12 @@ "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", "locator": "Table 4.2, printed page 41 (PDF page 42)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", "period": 2030, "fy": "2029-30", + "value_kind": "range_high", + "range_group": "centax_net_fy2029_scenarios", "value": 160000000, "value_raw": "+160m", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", @@ -344,11 +455,82 @@ "estimate_label": null, "pe_value": null, "gap": null, - "nearest_estimate": "thirty_hours_static_fy2029", - "nearest_pe_value": -505000000, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2029", + "nearest_pe_value": -556000000, + "nearest_gap": -4.475, + "status": "concept_mismatch", + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." + }, + { + "claim": "centax_static_central_fy2028", + "found_on": "2026-10-21", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table B.1 (actual ANI, central scenario), printed page 82 (PDF page 83)", + "metric": "revenue_change", + "basis": "static", + "program": "free_childcare_hours", + "period": 2029, + "fy": "2028-29", + "value_kind": "central", + "range_group": null, + "value": -880000000, + "value_raw": "-880m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": "thirty_hours_static_fy2028", + "estimate_label": "Static cost of the 30 funded hours, both limits removed, 2028-29", + "pe_value": -484000000, + "gap": -0.45, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "constructed", + "cause": "The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds £0.60bn in 2027-28 and £0.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at £0.96bn in 2027-28 and £1.12bn in 2029-30 as shipped, and £0.87bn and £0.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added).", + "pe_missing": null + }, + { + "claim": "centax_net_central_fy2028", + "found_on": "2026-10-21", + "source": "centax", + "publisher": "CenTax", + "title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "locator": "Table B.1 (actual ANI, central scenario), printed page 82 (PDF page 83)", + "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours", + "period": 2029, + "fy": "2028-29", + "value_kind": "central", + "range_group": null, + "value": -580000000, + "value_raw": "-580m", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "estimate": null, + "estimate_label": null, + "pe_value": null, + "gap": null, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": false, + "over_threshold": false, + "nearest_estimate": "both_schemes_dynamic_central_fy2028", + "nearest_pe_value": -533000000, + "nearest_gap": -0.081, "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition." + "cause": "CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below £100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below £100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching." }, { "claim": "cbp_net_table2_fy2026", @@ -359,20 +541,29 @@ "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "locator": "section 1.3.7, Table 2 (PDF page 8)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2027, "fy": "2026-27", + "value_kind": "point", + "range_group": null, "value": 70000000, "value_raw": "0.070", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2026", - "nearest_pe_value": -558000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2026", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2026-27", + "pe_value": -451000000, + "gap": -7.4429, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table2_fy2027", @@ -383,20 +574,29 @@ "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "locator": "section 1.3.7, Table 2 (PDF page 8)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2028, "fy": "2027-28", + "value_kind": "point", + "range_group": null, "value": 77000000, "value_raw": "0.077", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2027", - "nearest_pe_value": -593000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2027", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2027-28", + "pe_value": -479000000, + "gap": -7.2208, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table2_fy2028", @@ -407,20 +607,29 @@ "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "locator": "section 1.3.7, Table 2 (PDF page 8)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2029, "fy": "2028-29", + "value_kind": "point", + "range_group": null, "value": 91000000, "value_raw": "0.091", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2028", - "nearest_pe_value": -652000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2028", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2028-29", + "pe_value": -533000000, + "gap": -6.8571, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table2_fy2029", @@ -431,20 +640,29 @@ "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", "locator": "section 1.3.7, Table 2 (PDF page 8)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2030, "fy": "2029-30", + "value_kind": "point", + "range_group": null, "value": 132000000, "value_raw": "0.132", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2029", - "nearest_pe_value": -678000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2029", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2029-30", + "pe_value": -556000000, + "gap": -5.2121, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table3_fy2026", @@ -453,22 +671,31 @@ "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", - "locator": "section 1.3.7, Table 3 (PDF page 10)", + "locator": "section 1.4.6, Table 3 (PDF page 10)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2027, "fy": "2026-27", + "value_kind": "point", + "range_group": null, "value": 330000000, "value_raw": "0.330", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2026", - "nearest_pe_value": -558000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2026", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2026-27", + "pe_value": -451000000, + "gap": -2.3667, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table3_fy2027", @@ -477,22 +704,31 @@ "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", - "locator": "section 1.3.7, Table 3 (PDF page 10)", + "locator": "section 1.4.6, Table 3 (PDF page 10)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2028, "fy": "2027-28", + "value_kind": "point", + "range_group": null, "value": 349000000, "value_raw": "0.349", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2027", - "nearest_pe_value": -593000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2027", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2027-28", + "pe_value": -479000000, + "gap": -2.3725, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table3_fy2028", @@ -501,22 +737,31 @@ "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", - "locator": "section 1.3.7, Table 3 (PDF page 10)", + "locator": "section 1.4.6, Table 3 (PDF page 10)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2029, "fy": "2028-29", + "value_kind": "point", + "range_group": null, "value": 399000000, "value_raw": "0.399", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2028", - "nearest_pe_value": -652000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2028", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2028-29", + "pe_value": -533000000, + "gap": -2.3358, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null }, { "claim": "cbp_net_table3_fy2029", @@ -525,22 +770,31 @@ "publisher": "Centre for British Progress", "title": "Technical Appendix: underlying modelling and analysis for each of the policies in the Rates and Wrongs paper", "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", - "locator": "section 1.3.7, Table 3 (PDF page 10)", + "locator": "section 1.4.6, Table 3 (PDF page 10)", "metric": "revenue_change", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", "period": 2030, "fy": "2029-30", + "value_kind": "point", + "range_group": null, "value": 427000000, "value_raw": "0.427", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "estimate": null, - "estimate_label": null, - "pe_value": null, - "gap": null, - "nearest_estimate": "both_schemes_static_fy2029", - "nearest_pe_value": -678000000, - "status": "concept_mismatch", - "cause": "CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below £100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above.", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition." + "estimate": "both_schemes_dynamic_central_fy2029", + "estimate_label": "Cost of both schemes net of the labour-supply response (central elasticities), 2029-30", + "pe_value": -556000000, + "gap": -2.3021, + "gap_unbounded": false, + "group_gap": null, + "sign_disagrees": true, + "over_threshold": true, + "nearest_estimate": null, + "nearest_pe_value": null, + "nearest_gap": null, + "status": "comparable", + "cause": "Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below £100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126).", + "pe_missing": null } ] } diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index ee933b269..b4ed3a2c1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -207,12 +207,15 @@ Operating rules for the agent stage: - **Pre-publish comparables** (`sources/analysis--/`, lanes `analysis-comparables-uk` and `-us`; docs/COMPARABLES.md): one - directory per PolicyEngine analysis, holding every external estimate a - search of every listed producer found before the analysis was published, - the analysis's own pinned figures, and the cause of every gap over the - threshold. `scorecard_db/comparables.py` is the gate (an incomplete record - fails CI and the build) and writes `data/comparables/index.json`, which - policyengine-app-v2's CI reads before a research entry can merge. + directory per PolicyEngine analysis, holding every external estimate its + searches of every listed producer found (before publication, or in a + backfill, re-harvest or pre-promotion search after it), the analysis's own + pinned figures, and the cause of every gap over the threshold. + `scorecard_db/comparables.py` is the gate (an incomplete record fails CI + and the build) and writes `data/comparables/index.json`, which + policyengine-app-v2's CI (#1380 there) reads before a research entry can + merge and PolicyEngine/newsletters' CI (#29 there) reads before an edition + promotes an analysis. ### Staged for Chronicle, not claims diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index 14e83e287..c0dab9b72 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -7,60 +7,69 @@ before publishing an analysis - pe-wide"). The case that prompted it: the £100,000 childcare dashboard went live on 12 October 2026 with the 30 funded hours costing £0.43bn in 2027-28. CenTax had published £0.80bn in September, from HMRC -administrative data. The dashboard even listed CenTax as a benchmark, but nothing required the -gap to be explained, and the cause, a Microcosm hours draw ([microcosm#1126]), was found only -after Arun Advani pointed it out. Its record is +administrative data. The dashboard listed CenTax as a benchmark, but nothing required the gap to +be explained. Its cause, a Microcosm hours draw, had been filed as [microcosm#1126] on +7 October, five days before launch. The record is `sources/analysis-uk-childcare-100k-threshold/`; without the annotation it carries, the -validator below fails it on six rows. +validator below fails it on seven rows. [microcosm#1126]: https://github.com/PolicyEngine/microcosm/issues/1126 ## How it is enforced -1. **The search** runs before publishing, as the `policyengine-comparables-review` skill in - PolicyEngine/policyengine-skills (or a Subfleet research job from its brief). It writes a - directory here and opens a scorecard PR. +1. **The search** runs before publishing, as the `policyengine-comparables-review` skill + (PolicyEngine/policyengine-skills#100), or as a research job built from that skill's brief. It + writes a directory here and opens a scorecard PR. 2. **This repository's CI** validates every directory (`tests/test_comparables.py`, and the build, which refuses an incomplete record) and checks that `data/comparables/index.json` is exactly what the directories build. So everything on main is a complete record. -3. **policyengine-app-v2's CI** checks every research entry dated on or after its cutover: the - entry's `comparables` block must name a directory that the index on this repository's main - lists, with the same search date, producers and count. The scorecard PR therefore lands - before the registry entry can. -4. **After publishing**, a weekly launchd job (`comparables-reharvest`) searches again for every - analysis published in the last `reharvest_window_days` (183). A new comparable becomes a row - here; a new gap over the threshold also opens an issue for the analysis's owner. +3. **policyengine-app-v2's CI** (PolicyEngine/policyengine-app-v2#1380) checks every research + entry that is not on its list of entries published before the cutover. The entry's + `comparables` block must name a directory that the index on this repository's main lists, + with the same search date, kind, producers and count. The scorecard PR therefore lands before + the registry entry can. +4. **After publishing**, a weekly scheduled job searches again for every analysis published in the + last `reharvest_window_days` (183). A new comparable becomes a row here; a new gap over the + threshold also opens an issue for the analysis's owner. ## Before a push Publishing is not the only moment that counts. When an analysis that is already published is featured in a newsletter, a social campaign, a webinar or a press push, its comparables search is run again first (Max, 2026-10-21: "even if we've already published something, we should do a -fresh comp review before big pushes like newsletters"). The 21 October UK newsletter promoted the -childcare dashboard at £0.59bn. A fresh search that day would have put CenTax's £0.80bn static -cost for the 30 hours alone beside it. +fresh comp review before big pushes like newsletters"). The UK newsletter dated 21 October +(PolicyEngine/newsletters `editions/2026-10-20-uk.html`) featured the childcare dashboard at +£0.59bn for both schemes. A fresh search would have put CenTax's £0.80bn static cost for the +30 hours alone beside it. -- The search is appended to the analysis's `search_log.json` with `kind: prepromotion`, or the +- The search is appended to the analysis's `search_log.json` with `kind: prepromotion`, or is the analysis's first search if it has none yet (`backfill`). -- The push may go ahead only if a search is dated within `promotion_window_days` - (`data/comparables/policy.json`, 7 by default) before the push date, and no gap found by then - is still `pending`. `python -m scorecard_db.comparables fresh analysis-- --on - ` says yes or why not. -- PolicyEngine/newsletters' CI applies the same rule to every policyengine.org analysis an edition - links, for editions dated on or after its cutover. The index publishes the window - (`promotion_window_days`) and when each claim was found, so that check needs nothing but the - index. +- The push may go ahead only when: + - a search is dated on the push date or in the `promotion_window_days` before it + (`data/comparables/policy.json`, 7 by default); + - that search compared the results files the analysis reads now (its `results_pins` equal + the index's `current_pins`); + - no gap found by the push date is still `pending`. A pending gap blocks the push until its + cause is written. +- `python -m scorecard_db.comparables fresh analysis-- --on ` says yes or why + not. +- PolicyEngine/newsletters' CI (PolicyEngine/newsletters#29) applies the same rule to every + policyengine.org analysis an edition links, for editions dated on or after its cutover. + `tests/fixtures/comparables_fresh_cases.json` holds cases both implementations are tested + against. The index publishes the window, each search's pins and when each claim was found, so + that check needs nothing but the index. ## The directory `sources/analysis--/`, where `` is the analysis's slug on policyengine.org -(the apps.json slug, or the post's filename without `.md`). +(the apps.json slug, or the post's filename up to its first dot, lower-cased with underscores as +hyphens). | File | What it holds | |---|---| | `analysis.json` | The registry entry, scope, search terms, the policy worlds it scores, and every PolicyEngine estimate it publishes, each pinned to its results file by repo, commit, path, field and SHA-256 | -| `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`, `prepromotion`), who ran it, and one entry per listed producer | -| `manifest.jsonl` | One line per primary publication, with its SHA-256 | +| `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`, `prepromotion`), who ran it, the results files it compared, and one entry per listed producer | +| `manifest.jsonl` | One line per primary publication, with its SHA-256; every line is cited by a claim | | `claims_staged.jsonl` | Every comparable found, staged under the harvest rules | | `annotations.json` | The status and cause of anything that is not a plain comparison, each citing a basis | @@ -70,6 +79,24 @@ Start one with: PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk --kind app --slug ``` +The JSON files are strict: a repeated key fails the record. + +### The estimates + +Each estimate in `pe_estimates` names its `world`, `metric`, `unit_concept`, `period` and +`time_basis`, and two conditions: + +- `conditions.basis`: `static`, `post_behavioural`, `forecast` or `outturn`; +- `conditions.program`: a slug for what is costed (`thirty_hours`, `both_schemes`). + +When `conditions.fy` names a fiscal year, `period` is its end year and `time_basis` is +`fiscal_year`. Each estimate is stated in PolicyEngine's own orientation for its metric; when the +results file prints it the other way round (a cost as a positive number), the estimate carries +`sign: -1` and a `sign_basis`. + +An estimate that a rerun replaced moves to `superseded_estimates` with `superseded_on`, and stays +on the scorecard as history. + ### The search `data/comparables/producers.json` lists the producers for each country. A search logs every @@ -82,64 +109,105 @@ producer whose `added` date is on or before the search date, with one of: - `not_applicable`: only when the producer's `applies_to` excludes the analysis's scope (the Scottish Fiscal Commission for a UK-wide analysis, state fiscal notes for a federal one). +The searches are in date order, and each is dated on a real day that is no later than today and +no earlier than the country's producer list. Their kinds follow the analysis's life: + +| Kind | When | +|---|---| +| `prepublish` | The first search, on or before `published` | +| `backfill` | The first search of an analysis already published, on or after `published` | +| `reharvest` | A later search, after `published` and after the first search | +| `prepromotion` | A later search before a push, after `published` and after the first search | + +Each search records `results_pins`, the results files it compared (`repo@commit:sha256`; `python +-m scorecard_db.comparables pins ` prints them). The latest search's pins are the +estimates' current pins: a new results file needs a new search. + `open_search` and `academic` are on every list: a comparable from a producer not on the list is -logged under them and keeps its producer's own slug. When an open search keeps finding one -producer, add it to the list with a `why` (the first search added DfE this way). +found under them and keeps its producer's own slug as `source`. A claim from a listed producer is +logged under that producer's entry. `source` is never `open_search`, `academic` or +`policyengine`. When an open search keeps finding one producer, add it to the list with a `why` +(the first search added DfE this way). ### The claims Staged under the harvest rules (`sources/harvest-uk-ab2025-2026-09-24/README.md`), checked mechanically where they can be: -- **Verbatim values.** `value_raw` is the string as printed; `value` is it under a registered - `normalization`; the validator parses one back to the other, and `quote` must contain the - printed value. A claim is never re-signed: its sign convention travels in - `conditions.sign_convention`, in words `scorecard_db/signs.py` reads. +- **Verbatim values.** `value_raw` is the number as printed: a minus sign, the word "minus" or + parentheses make it negative, and a scale printed after it (`m`, `bn`, `%`) must match the + `normalization`. `value` is it under a registered `normalization`; the validator parses one back + to the other, for a chart reading too. `quote` must print the value as a whole number with its + sign, except for a chart reading. A claim is never re-signed: its sign convention travels in + `conditions.sign_convention`, in words `scorecard_db/signs.py` reads. A compared change metric + needs a convention that starts `positive =` and says one thing. +- **The same identity as an estimate**: `metric`, `unit_concept`, `period`, `time_basis`, + `conditions.basis` and `conditions.program`, with the same `conditions.fy` rule. - **Registered vocabulary** for `metric` and `unit_concept`, or `null` with a proposal (such a claim is staged but waits for its vocabulary decision before it reaches the database). - **Named baselines**: `baseline_policy` is a label in `scorecard_db/baselines.py`, or null for - current law. + current law. A world in `analysis.json` may name its `baseline` the same way. - **Attribution and benchmark class.** Only a producer's own estimate (`attribution: own`) is compared. An administrative level is calibration material and belongs in a producer-level source, where `relationships.py` assigns it. -- **Primaries, hashed**: `publication.primary_sha256` is a line in `manifest.jsonl`. +- **Primaries, hashed**: `publication.primary_sha256` is a line in `manifest.jsonl`, and + `publication.date` is no later than the day the claim was found. +- **Ranges**: a printed range is two claims, `value_kind` `range_low` and `range_high`, sharing a + `conditions.range_group`, the same identity and the same `compare_to`. ### The comparison A claim either names the PolicyEngine estimate it is compared with (`compare_to`), or says in -`pe_missing` why there is none. A compared claim has the estimate's metric, unit, period and time -basis. A claim that has all four in common with a published estimate but is not compared -cannot be waved off with a sentence. It names that estimate in `nearest_estimate`, and an -annotation with status `concept_mismatch` (or `pe_gap`) and a basis says why the two differ: a -net-of-behaviour cost beside a static one, for example. On the scorecard it then appears beside -that estimate, labelled with the mismatch, rather than blank. When the two score different policy worlds, `compare_to.across_worlds` gives the reason -the quantities match and its basis, and the comparison is `constructed`, not `comparable`. +`pe_missing` why there is none. + +- A compared claim has the estimate's full identity and baseline, is the producer's own, and is + not cumulative. When the two score different policy worlds, `compare_to.across_worlds` gives + the reason the quantities match and its basis, and the comparison is `constructed`, not + `comparable`. +- An uncompared claim that has an estimate near it (the same period, and the same metric and + unit where the claim names them) cannot be waved off with a sentence. It names that estimate + in `nearest_estimate`, and an annotation with status `concept_mismatch` (or `pe_gap`) and a + basis says why the two differ: a net-of-behaviour cost beside a static one, for example. On + the scorecard it then appears beside that estimate, labelled with the mismatch, rather than + blank. A claim that is the same quantity as its nearest estimate must be compared instead. The gap is PolicyEngine's value over the external value, minus one, with the external value in -PolicyEngine's orientation (`signs.py`; an unsigned magnitude compares absolute values). A -nonzero estimate against a published zero is over any threshold. Each PolicyEngine estimate is -stated in PolicyEngine's own orientation for its metric; when the results file prints it the -other way round (a cost as a positive number), the estimate carries `sign: -1` and a -`sign_basis`. +PolicyEngine's orientation (`signs.py`; an unsigned level compares absolute values). A +nonzero estimate against a published zero is over any threshold, and so is any pair whose signs +disagree. A range is over only when the estimate lies outside it by more than the threshold, +measured from the nearer bound. ### The threshold -`data/comparables/policy.json` `gap_threshold`, 0.25 by default. A compared claim whose absolute -gap exceeds it needs an annotation with `explains_gap: true`: its `cause`, and a `basis` that is a -GitHub issue or pull request, engine metadata pinned to a commit, a document, or a measured -diagnostic. This is the scorecard's existing rule against fabricated mechanisms: a cause that -cannot cite one of those does not ship. A miss is allowed; an unexplained one is not. +`data/comparables/policy.json` `gap_threshold`, 0.25 by default and always below 1, so that a +sign disagreement is always over it. A compared claim whose absolute gap exceeds it needs an +annotation with `explains_gap: true`: its `cause`, and a `basis` that is one of: + +- a GitHub issue or pull request URL; +- engine metadata, as a GitHub blob pinned to a commit or a file in this repository; +- a document or a measured diagnostic, as a URL with a path or a file in this repository. -A comparable that a post-publication re-harvest finds may instead carry a `pending` annotation -naming the issue opened for the owner; the index counts it until the cause is written. +This is the scorecard's existing rule against fabricated mechanisms: a cause that cannot cite one +of those does not ship. A miss is allowed; an unexplained one is not. + +A comparable that a search after publication finds (`backfill`, `reharvest` or `prepromotion`) +may instead carry a `pending` annotation naming the `tracking_issue` opened for the owner. The +index counts it, and it blocks any push, until the cause is written. Nothing found before +publication can be pending. ### The database `scorecard_db/ingest_analysis_comparables.py` (build step `analysis_comparables`) puts every -record on the scorecard: claims held out (another producer's estimate is a comparator, never a -calibration target), with `conditions.analysis` naming the analysis; one result per compared -claim, the analysis's own figure with its record's status; uncompared claims published without a -result. Lanes `analysis-comparables-uk` and `analysis-comparables-us` show the tally. +record on the scorecard. Claims are held out (another producer's estimate is a comparator, never +a calibration target), with `conditions.analysis` naming the analysis. Each claim's result is: + +- for a compared claim, the analysis's own figure with its record's status (a pending gap shows + as `comparable` or `constructed`, with an annotation naming its tracking issue); +- for a claim beside its nearest estimate, that estimate's figure, labelled with the mismatch; +- for any other claim, none. + +A superseded estimate adds a history result. Lanes `analysis-comparables-uk` and +`analysis-comparables-us` show the tally. ## The index and the app-v2 record @@ -164,9 +232,10 @@ PYTHONPATH=. python -m scorecard_db.comparables record analysis-uk- } ``` -An entry that publishes no estimate (an event, an announcement) declares -`{"status": "no_estimate", "reason": "..."}` instead; app-v2 refuses that for an entry tagged -`policy`. +A post that publishes no estimate (an event, an announcement) declares +`{"status": "no_estimate", "reason": "..."}` instead. That is allowed only for a post tagged as +one of those kinds (`org`, `event`, `giving`, `conferences`, `year-in-review`, `april-fools`) and +no analysis tag; an app never declares it. ## Re-harvest support diff --git a/sources/analysis-uk-childcare-100k-threshold/README.md b/sources/analysis-uk-childcare-100k-threshold/README.md index ec64e2202..43a33eb89 100644 --- a/sources/analysis-uk-childcare-100k-threshold/README.md +++ b/sources/analysis-uk-childcare-100k-threshold/README.md @@ -7,18 +7,23 @@ backfill: the dashboard was published on 12 October 2026, before the gate existe in 2027-28 and £0.505bn in 2029-30, from `PolicyEngine/uk-childcare-100k-threshold` at `f25f0db` (`data/results.json`). - **What the search found** (2026-10-21, every UK producer): CenTax's Table 4.2 (static and net, - three scenarios) and the Centre for British Progress's net fiscal impact. DfE's FOI estimate - and the Conservative Party's £700m are logged as found but not staged, because neither has a - readable primary. -- **The gap**: CenTax's central static cost is 46% (2027-28) and 49% (2029-30) above the - published figure. The annotation cites microcosm#1126, the Microcosm hours draw that put - families' use of the extended hours at about 15 of 30, and the dashboard's rerun on the - corrected draw (uk-childcare-100k-threshold#8). + central with the high- and low-cost scenarios as a range, for 2027-28 and 2029-30) and Table + B.1's 2028-29 row, and the Centre for British Progress's net fiscal impact (Tables 2 and 3, + 2026-27 to 2029-30). DfE's FOI estimate and the Conservative Party's £700m are logged as found + but not staged, because neither has a readable primary. +- **The gap**: the published static cost is 45.8% (2027-28) and 48.5% (2029-30) below CenTax's + central static cost (£0.80bn and £0.98bn), and below the low-cost end of CenTax's range too. The + annotation cites microcosm#1126, the Microcosm hours draw that put families' use of the + extended hours at about 15 of 30, and the dashboard's rerun on the corrected draw + (uk-childcare-100k-threshold#8). CenTax's net figures sit beside the dashboard's net cost as a + concept mismatch; CBP's net gain disagrees in sign with it, which the bunching annotation + explains. - **Status**: the dashboard was taken off policyengine.org on 21 October 2026 (PolicyEngine/policyengine-app-v2#1379) while its costing is revised. The record stays: the - miss stays on the page. A republished version needs its own prepublish search appended to - `search_log.json`. Its new figures are pinned as further `pe_estimates`, and the - re-harvest picks the dashboard up again once it is back in `apps.json`. + miss stays on the page. A republished version moves these figures to `superseded_estimates`, + pins its new ones in `pe_estimates`, and needs a new search against its new results files + (kind `reharvest`) before it goes back up. The re-harvest picks the dashboard up again once it + is back in `apps.json`. The same CenTax and CBP publications are staged at producer level in `sources/centax-childcare-100k/` and `sources/cbp-rates-and-wrongs/` (#207), with diff --git a/tests/fixtures/comparables_fresh_cases.json b/tests/fixtures/comparables_fresh_cases.json new file mode 100644 index 000000000..75ae48b4a --- /dev/null +++ b/tests/fixtures/comparables_fresh_cases.json @@ -0,0 +1,2354 @@ +[ + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 3 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-24", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-25", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-26", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-10-30", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": true, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-02", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-10-31", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-02", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + }, + { + "entry": { + "comparisons": [ + { + "claim": "c0", + "found_on": "2026-11-04", + "status": "pending" + } + ], + "current_pins": [ + "PolicyEngine/test@bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + ], + "searches": [ + { + "date": "2026-11-03", + "kind": "reharvest", + "results_pins": [ + "x" + ] + } + ] + }, + "fresh": false, + "on": "2026-11-02", + "window": 7 + } +] From 71bd48558980d7877bd2c7945e0afe7232bfba75 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:03:55 -0400 Subject: [PATCH 08/15] Publish the entry rules in the comparables index, and regenerate the feeds The index now carries the tag rules that decide which policyengine.org entries need a record, so app-v2 and newsletters can check they apply the same ones. The promotion window is bounded to 1-90 days, matching the newsletters check. Populations, lanes and the app copies are regenerated for the childcare record's 22 claims. Co-Authored-By: Claude Opus 5.5 --- app/public/data/lanes.json | 6 +- app/public/data/populations.json | 716 +++++++++++++------- data/comparables/index.json | 42 ++ data/lanes.json | 6 +- data/populations.json | 716 +++++++++++++------- scorecard_db/comparables.py | 345 +++++++--- scorecard_db/ingest_analysis_comparables.py | 4 +- tests/test_comparables.py | 376 ++++++++-- tests/test_export_populations.py | 6 +- 9 files changed, 1530 insertions(+), 687 deletions(-) diff --git a/app/public/data/lanes.json b/app/public/data/lanes.json index cdef6030b..1c5cdb08c 100644 --- a/app/public/data/lanes.json +++ b/app/public/data/lanes.json @@ -4,7 +4,7 @@ "country": "US | UK | BE | NZ \u2014 the model instance the lane feeds (issue #42)", "note": "Repo-committed status feed (issue #7); graduate to Supabase when lane count makes commits noisy. Every lane writes its transition here." }, - "updated": "2026-10-21", + "updated": "2026-10-22", "lanes": [ { "id": "urban-sotsn", @@ -587,7 +587,7 @@ "stage": "computed", "running": false, "updated": "2026-10-21", - "note": "1 analyses searched, 20 claims, 20 with the analysis's own figure beside them" + "note": "1 analysis searched, 22 claims, 22 PolicyEngine results beside them" }, { "id": "analysis-comparables-us", @@ -598,7 +598,7 @@ "stage": "registered", "running": false, "updated": "2026-10-21", - "note": "0 analyses searched, 0 claims, 0 with the analysis's own figure beside them" + "note": "0 analyses searched, 0 claims, 0 PolicyEngine results beside them" } ] } diff --git a/app/public/data/populations.json b/app/public/data/populations.json index 9208c3b1f..2b9d79ab1 100644 --- a/app/public/data/populations.json +++ b/app/public/data/populations.json @@ -1,8 +1,8 @@ { - "built": "2026-10-21", + "built": "2026-10-22", "note": "Non-Urban populations exported from scorecard.db: the populace reform-validation registry (issue #20) plus the compute campaign's attached comparisons (US: TPC/CPSP/PWBM/CBO/JCT; UK: the HMRC ready-reckoner family and the complete OBR Policy measures database, including historical HM Treasury scores; BE: JRC EUROMOD-BE claims with demo-grade Axiom concept-mismatch attachments and the 2026 PIT-reform scorekeeper claims; NZ: official budget scores). Claims explicitly opted into pre-result display remain in the feed with latest status not_computed and empty history. Recorded result statuses and calibration relationships are verbatim; nothing here is a pass/fail grade.", "summary": { - "claims": 29737, + "claims": 29739, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -16,8 +16,7 @@ "census": 104, "census_pep": 1, "census_stc": 88, - "centax": 92, - "centre_for_british_progress": 8, + "centax": 80, "co_admin": 3, "co_fiscal_note": 1, "cour_des_comptes": 1, @@ -106,17 +105,21 @@ "wbg": 4, "wi_admin": 1 }, + "by_source_in_comparables_records": { + "centax": 14, + "centre_for_british_progress": 8 + }, "by_latest_status": { - "comparable": 42, - "concept_mismatch": 31, - "constructed": 1668, + "comparable": 50, + "concept_mismatch": 24, + "constructed": 1669, "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28965, + "same": 28967, "unknown": 269 }, "by_country": { @@ -135,9 +138,9 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29181, - "with_result": 2182, - "with_value": 1197, + "claims": 29183, + "with_result": 2184, + "with_value": 1199, "without_result": 26999, "multi_release_claims": 0 }, @@ -25513,7 +25516,7 @@ } }, { - "claim_id": "4a0f85b93849b6dd88c2", + "claim_id": "fe4ce312800fe0924aba", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25533,17 +25536,19 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_central_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", "scenario": "central", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-520m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -520000000.0, @@ -25553,34 +25558,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.8346153846153846, - "delta": 86000000.0 + "ratio": 0.9211538461538461, + "delta": 41000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25589,7 +25594,7 @@ "diagnosis": null }, { - "claim_id": "c7b61b17226c7541f264", + "claim_id": "15f8e74fa4e34a7b9554", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25612,6 +25617,7 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", @@ -25619,6 +25625,7 @@ "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-800m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -800000000.0, @@ -25633,11 +25640,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25651,11 +25658,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25664,7 +25671,7 @@ "diagnosis": null }, { - "claim_id": "4b5f3820c010baa7f581", + "claim_id": "6ad874aab66d820fdc3b", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25684,17 +25691,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_high_cost_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2027_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-610m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -610000000.0, @@ -25704,34 +25714,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.7114754098360656, - "delta": 176000000.0 + "ratio": 0.7852459016393443, + "delta": 131000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25740,7 +25750,7 @@ "diagnosis": null }, { - "claim_id": "529d98a827c2481cd335", + "claim_id": "0b6aa332c68212293288", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25763,13 +25773,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2027_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-820m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -820000000.0, @@ -25784,11 +25797,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25802,11 +25815,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25815,7 +25828,7 @@ "diagnosis": null }, { - "claim_id": "abb782333de07295c157", + "claim_id": "283fbb3d3bf981674490", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25835,17 +25848,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_low_cost_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2027_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "+140m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": 140000000.0, @@ -25855,34 +25871,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": -3.1, - "delta": -574000000.0 + "ratio": -3.4214285714285713, + "delta": -619000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25891,7 +25907,7 @@ "diagnosis": null }, { - "claim_id": "14a2b2f5e58c26103463", + "claim_id": "7b45daef72f83e1a2960", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25914,13 +25930,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2027_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-580m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -580000000.0, @@ -25935,11 +25954,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25953,11 +25972,166 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], + "diagnosis": null + }, + { + "claim_id": "65b4e630a623a9d8b487", + "source": "centax", + "country": "UK", + "source_column": "Central Scenario, tax year 2029: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2028", + "basis": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "central", + "fy": "2028-29", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-580m" + }, + "measure": null, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -533000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.9189655172413793, + "delta": 47000000.0 + }, + "results": [ + { + "value": -533000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], + "diagnosis": null + }, + { + "claim_id": "14d4050ab5c339dd8442", + "source": "centax", + "country": "UK", + "source_column": "Central Scenario, tax year 2029: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2028", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "central", + "fy": "2028-29", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-880m" + }, + "measure": null, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -880000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -880000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -484000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2028\", \"field\": \"budget.gross_bn.thirty_hours.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.55, + "delta": 396000000.0 + }, + "results": [ + { + "value": -484000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2028\", \"field\": \"budget.gross_bn.thirty_hours.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25966,7 +26140,7 @@ "diagnosis": null }, { - "claim_id": "db96c1be69d70c9783dd", + "claim_id": "f4397e5778474de9cc1c", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -25986,17 +26160,19 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_central_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", "scenario": "central", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-640m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -640000000.0, @@ -26006,34 +26182,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.7890625, - "delta": 135000000.0 + "ratio": 0.86875, + "delta": 84000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26042,7 +26218,7 @@ "diagnosis": null }, { - "claim_id": "145a9fa05c8bfec614be", + "claim_id": "5c56ff121ba5f93053f1", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26065,6 +26241,7 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", @@ -26072,6 +26249,7 @@ "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-980m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -980000000.0, @@ -26086,11 +26264,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26104,11 +26282,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -26117,7 +26295,7 @@ "diagnosis": null }, { - "claim_id": "64ea2993d719166f5714", + "claim_id": "f4d26f478523c790d7a0", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -26137,17 +26315,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_high_cost_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2029_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-770m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -770000000.0, @@ -26157,34 +26338,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.6558441558441559, - "delta": 265000000.0 + "ratio": 0.7220779220779221, + "delta": 214000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26193,7 +26374,7 @@ "diagnosis": null }, { - "claim_id": "1ab38121c6bdb17bcb7c", + "claim_id": "1449769add785901db25", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26216,13 +26397,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2029_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-1,010m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -1010000000.0, @@ -26237,11 +26421,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26255,11 +26439,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -26268,7 +26452,7 @@ "diagnosis": null }, { - "claim_id": "f460a20fb80ee6108e62", + "claim_id": "d8af9ff525d462679608", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -26288,17 +26472,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_low_cost_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2029_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "+160m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": 160000000.0, @@ -26308,34 +26495,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": -3.15625, - "delta": -665000000.0 + "ratio": -3.475, + "delta": -716000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26344,7 +26531,7 @@ "diagnosis": null }, { - "claim_id": "2ad7d14489e0a073fb55", + "claim_id": "53c7008af6b8a702630a", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26367,13 +26554,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2029_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-700m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -700000000.0, @@ -26388,11 +26578,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26406,11 +26596,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -27377,7 +27567,7 @@ "diagnosis": null }, { - "claim_id": "eef80e8be4769cc2ce16", + "claim_id": "3e0f4ed92a011b5a1d9a", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2026/27", @@ -27396,64 +27586,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2026", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2026-27", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.330" + "value_verbatim": "0.070" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 330000000.0, + "external_value": 70000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 330000000.0, + "external_value_pe_sign": 70000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.690909090909091, - "delta": -888000000.0 + "status_effective": "comparable", + "ratio": -6.442857142857143, + "delta": -521000000.0 }, "results": [ { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "f40fe6519a80e65479f2", + "claim_id": "9def514bdb6ddd5f7abe", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2026/27", @@ -27472,64 +27662,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2026", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2026-27", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.070" + "value_verbatim": "0.330" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 70000000.0, + "external_value": 330000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 70000000.0, + "external_value_pe_sign": 330000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.9714285714285715, - "delta": -628000000.0 + "status_effective": "comparable", + "ratio": -1.3666666666666667, + "delta": -781000000.0 }, "results": [ { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "123f8c0626dfc2a46e5d", + "claim_id": "b4b2459d4a9c757a940f", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2027/28", @@ -27548,64 +27738,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2027", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2027-28", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.349" + "value_verbatim": "0.077" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 349000000.0, + "external_value": 77000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 349000000.0, + "external_value_pe_sign": 77000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.6991404011461317, - "delta": -942000000.0 + "status_effective": "comparable", + "ratio": -6.220779220779221, + "delta": -556000000.0 }, "results": [ { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "ba4937e8a058012d9f58", + "claim_id": "deadc9241b4fd49010e2", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2027/28", @@ -27624,64 +27814,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2027", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2027-28", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.077" + "value_verbatim": "0.349" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 77000000.0, + "external_value": 349000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 77000000.0, + "external_value_pe_sign": 349000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.701298701298701, - "delta": -670000000.0 + "status_effective": "comparable", + "ratio": -1.3724928366762177, + "delta": -828000000.0 }, "results": [ { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "985648c9838b5184dc10", + "claim_id": "9af08733f1c6826235aa", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2028/29", @@ -27700,64 +27890,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2028", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2028-29", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.399" + "value_verbatim": "0.091" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 399000000.0, + "external_value": 91000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 399000000.0, + "external_value_pe_sign": 91000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.6340852130325814, - "delta": -1051000000.0 + "status_effective": "comparable", + "ratio": -5.857142857142857, + "delta": -624000000.0 }, "results": [ { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "d6f07da73635a2c1e185", + "claim_id": "bf09bd19074e21cabac6", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2028/29", @@ -27776,64 +27966,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2028", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2028-29", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.091" + "value_verbatim": "0.399" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 91000000.0, + "external_value": 399000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 91000000.0, + "external_value_pe_sign": 399000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.164835164835165, - "delta": -743000000.0 + "status_effective": "comparable", + "ratio": -1.3358395989974938, + "delta": -932000000.0 }, "results": [ { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "3a5f40190cc9637435d5", + "claim_id": "6587454d24d586b6fdf8", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2029/30", @@ -27854,16 +28044,16 @@ "analysis": "uk/childcare-100k-threshold", "analysis_claim": "cbp_net_table3_fy2029", "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2029-30", "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", "value_verbatim": "0.427" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", "external_value": 427000000.0, @@ -27873,43 +28063,43 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.5878220140515222, - "delta": -1105000000.0 + "status_effective": "comparable", + "ratio": -1.3021077283372364, + "delta": -983000000.0 }, "results": [ { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "68b30471cf9f5a87368c", + "claim_id": "c7aee9e51cd5570028cc", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2029/30", @@ -27930,16 +28120,16 @@ "analysis": "uk/childcare-100k-threshold", "analysis_claim": "cbp_net_table2_fy2029", "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2029-30", "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", "value_verbatim": "0.132" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", "external_value": 132000000.0, @@ -27949,37 +28139,37 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -5.136363636363637, - "delta": -810000000.0 + "status_effective": "comparable", + "ratio": -4.212121212121212, + "delta": -688000000.0 }, "results": [ { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null diff --git a/data/comparables/index.json b/data/comparables/index.json index cbc119e94..1599bc131 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -3,6 +3,48 @@ "schema": "comparables/index/1", "gap_threshold": 0.25, "promotion_window_days": 7, + "entry_rules": { + "analysis_tags": [ + "2024-election", + "autumn-budget", + "behavioral-responses", + "benefit", + "benefit-access", + "child-tax-credit", + "election", + "healthcare", + "inflation", + "interactives", + "policy", + "reconciliation", + "tariffs", + "tax", + "technical", + "vat" + ], + "no_estimate_tags": [ + "april-fools", + "conferences", + "event", + "giving", + "org", + "year-in-review" + ], + "country_tags": [ + "uk", + "us", + "ca", + "ng", + "il" + ], + "default_country": "us", + "search_kinds": [ + "prepublish", + "backfill", + "reharvest", + "prepromotion" + ] + }, "analyses": { "analysis-uk-childcare-100k-threshold": { "id": "uk/childcare-100k-threshold", diff --git a/data/lanes.json b/data/lanes.json index cdef6030b..1c5cdb08c 100644 --- a/data/lanes.json +++ b/data/lanes.json @@ -4,7 +4,7 @@ "country": "US | UK | BE | NZ \u2014 the model instance the lane feeds (issue #42)", "note": "Repo-committed status feed (issue #7); graduate to Supabase when lane count makes commits noisy. Every lane writes its transition here." }, - "updated": "2026-10-21", + "updated": "2026-10-22", "lanes": [ { "id": "urban-sotsn", @@ -587,7 +587,7 @@ "stage": "computed", "running": false, "updated": "2026-10-21", - "note": "1 analyses searched, 20 claims, 20 with the analysis's own figure beside them" + "note": "1 analysis searched, 22 claims, 22 PolicyEngine results beside them" }, { "id": "analysis-comparables-us", @@ -598,7 +598,7 @@ "stage": "registered", "running": false, "updated": "2026-10-21", - "note": "0 analyses searched, 0 claims, 0 with the analysis's own figure beside them" + "note": "0 analyses searched, 0 claims, 0 PolicyEngine results beside them" } ] } diff --git a/data/populations.json b/data/populations.json index 9208c3b1f..2b9d79ab1 100644 --- a/data/populations.json +++ b/data/populations.json @@ -1,8 +1,8 @@ { - "built": "2026-10-21", + "built": "2026-10-22", "note": "Non-Urban populations exported from scorecard.db: the populace reform-validation registry (issue #20) plus the compute campaign's attached comparisons (US: TPC/CPSP/PWBM/CBO/JCT; UK: the HMRC ready-reckoner family and the complete OBR Policy measures database, including historical HM Treasury scores; BE: JRC EUROMOD-BE claims with demo-grade Axiom concept-mismatch attachments and the 2026 PIT-reform scorekeeper claims; NZ: official budget scores). Claims explicitly opted into pre-result display remain in the feed with latest status not_computed and empty history. Recorded result statuses and calibration relationships are verbatim; nothing here is a pass/fail grade.", "summary": { - "claims": 29737, + "claims": 29739, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -16,8 +16,7 @@ "census": 104, "census_pep": 1, "census_stc": 88, - "centax": 92, - "centre_for_british_progress": 8, + "centax": 80, "co_admin": 3, "co_fiscal_note": 1, "cour_des_comptes": 1, @@ -106,17 +105,21 @@ "wbg": 4, "wi_admin": 1 }, + "by_source_in_comparables_records": { + "centax": 14, + "centre_for_british_progress": 8 + }, "by_latest_status": { - "comparable": 42, - "concept_mismatch": 31, - "constructed": 1668, + "comparable": 50, + "concept_mismatch": 24, + "constructed": 1669, "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28965, + "same": 28967, "unknown": 269 }, "by_country": { @@ -135,9 +138,9 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29181, - "with_result": 2182, - "with_value": 1197, + "claims": 29183, + "with_result": 2184, + "with_value": 1199, "without_result": 26999, "multi_release_claims": 0 }, @@ -25513,7 +25516,7 @@ } }, { - "claim_id": "4a0f85b93849b6dd88c2", + "claim_id": "fe4ce312800fe0924aba", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25533,17 +25536,19 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_central_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", "scenario": "central", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-520m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -520000000.0, @@ -25553,34 +25558,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.8346153846153846, - "delta": 86000000.0 + "ratio": 0.9211538461538461, + "delta": 41000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25589,7 +25594,7 @@ "diagnosis": null }, { - "claim_id": "c7b61b17226c7541f264", + "claim_id": "15f8e74fa4e34a7b9554", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25612,6 +25617,7 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", @@ -25619,6 +25625,7 @@ "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-800m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -800000000.0, @@ -25633,11 +25640,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25651,11 +25658,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25664,7 +25671,7 @@ "diagnosis": null }, { - "claim_id": "4b5f3820c010baa7f581", + "claim_id": "6ad874aab66d820fdc3b", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25684,17 +25691,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_high_cost_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2027_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-610m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -610000000.0, @@ -25704,34 +25714,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.7114754098360656, - "delta": 176000000.0 + "ratio": 0.7852459016393443, + "delta": 131000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25740,7 +25750,7 @@ "diagnosis": null }, { - "claim_id": "529d98a827c2481cd335", + "claim_id": "0b6aa332c68212293288", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25763,13 +25773,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2027_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-820m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -820000000.0, @@ -25784,11 +25797,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25802,11 +25815,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25815,7 +25828,7 @@ "diagnosis": null }, { - "claim_id": "abb782333de07295c157", + "claim_id": "283fbb3d3bf981674490", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2028: Net Fiscal Cost (\u00a3)", @@ -25835,17 +25848,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_low_cost_fy2027", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2027-28", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2027_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "+140m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": 140000000.0, @@ -25855,34 +25871,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": -3.1, - "delta": -574000000.0 + "ratio": -3.4214285714285713, + "delta": -619000000.0 }, "results": [ { - "value": -434000000.0, + "value": -479000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -25891,7 +25907,7 @@ "diagnosis": null }, { - "claim_id": "14a2b2f5e58c26103463", + "claim_id": "7b45daef72f83e1a2960", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2028: Static Cost (\u00a3)", @@ -25914,13 +25930,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2027-28", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2027_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-580m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -580000000.0, @@ -25935,11 +25954,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -25953,11 +25972,166 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2027\", \"field\": \"budget.gross_bn.thirty_hours.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." + ], + "baseline": "current_law", + "status_effective": "constructed" + } + ], + "diagnosis": null + }, + { + "claim_id": "65b4e630a623a9d8b487", + "source": "centax", + "country": "UK", + "source_column": "Central Scenario, tax year 2029: Net Fiscal Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_central_fy2028", + "basis": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "central", + "fy": "2028-29", + "geography": "England", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-580m" + }, + "measure": null, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -533000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch", + "ratio": 0.9189655172413793, + "delta": 47000000.0 + }, + "results": [ + { + "value": -533000000.0, + "status": "concept_mismatch", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." + ], + "baseline": "current_law", + "status_effective": "concept_mismatch" + } + ], + "diagnosis": null + }, + { + "claim_id": "14d4050ab5c339dd8442", + "source": "centax", + "country": "UK", + "source_column": "Central Scenario, tax year 2029: Static Cost (\u00a3)", + "name": "Removing the childcare cliff-edge: impacts and cost of reform", + "window": "", + "publication_title": "Removing the childcare cliff-edge: impacts and cost of reform", + "url": "https://centax.org.uk/wp-content/uploads/2026/09/AdvaniFlewPepin-HallSummers2026_Removing-the-childcare-cliff-edge.pdf", + "metric": "revenue_change", + "unit_concept": "gbp", + "value_kind": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "period_start": null, + "period_end": null, + "geography": "England", + "program": "free_childcare_hours", + "conditions": { + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_static_central_fy2028", + "basis": "static", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "central", + "fy": "2028-29", + "geography": "England", + "program": "free_childcare_hours", + "scenario": "central", + "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", + "value_verbatim": "-880m" + }, + "measure": null, + "reform_framework": "policy_ref", + "reform_key": "db11caf3e07ba4d1", + "external_value": -880000000.0, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -880000000.0, + "calibration_relationship": "held_out", + "claim_baseline": "current_law", + "latest": { + "value": -484000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2028\", \"field\": \"budget.gross_bn.thirty_hours.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", + "policyengine_variables": [], + "computed_at": "2026-10-09T11:50:21+00:00", + "annotations": [ + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." + ], + "baseline": "current_law", + "status_effective": "constructed", + "ratio": 0.55, + "delta": 396000000.0 + }, + "results": [ + { + "value": -484000000.0, + "status": "constructed", + "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", + "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", + "release": "1002T230158Z", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2028\", \"field\": \"budget.gross_bn.thirty_hours.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -25966,7 +26140,7 @@ "diagnosis": null }, { - "claim_id": "db96c1be69d70c9783dd", + "claim_id": "f4397e5778474de9cc1c", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -25986,17 +26160,19 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_central_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", "scenario": "central", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-640m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -640000000.0, @@ -26006,34 +26182,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.7890625, - "delta": 135000000.0 + "ratio": 0.86875, + "delta": 84000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26042,7 +26218,7 @@ "diagnosis": null }, { - "claim_id": "145a9fa05c8bfec614be", + "claim_id": "5c56ff121ba5f93053f1", "source": "centax", "country": "UK", "source_column": "Central Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26065,6 +26241,7 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "central", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", @@ -26072,6 +26249,7 @@ "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-980m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -980000000.0, @@ -26086,11 +26264,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26104,11 +26282,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -26117,7 +26295,7 @@ "diagnosis": null }, { - "claim_id": "64ea2993d719166f5714", + "claim_id": "f4d26f478523c790d7a0", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -26137,17 +26315,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_high_cost_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2029_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-770m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -770000000.0, @@ -26157,34 +26338,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": 0.6558441558441559, - "delta": 265000000.0 + "ratio": 0.7220779220779221, + "delta": 214000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26193,7 +26374,7 @@ "diagnosis": null }, { - "claim_id": "1ab38121c6bdb17bcb7c", + "claim_id": "1449769add785901db25", "source": "centax", "country": "UK", "source_column": "High-Cost Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26216,13 +26397,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_low", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2029_scenarios", "scenario": "high_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-1,010m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -1010000000.0, @@ -26237,11 +26421,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26255,11 +26439,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -26268,7 +26452,7 @@ "diagnosis": null }, { - "claim_id": "f460a20fb80ee6108e62", + "claim_id": "d8af9ff525d462679608", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2030: Net Fiscal Cost (\u00a3)", @@ -26288,17 +26472,20 @@ "conditions": { "analysis": "uk/childcare-100k-threshold", "analysis_claim": "centax_net_low_cost_fy2029", - "basis": "net of intensive and extensive behavioural margins", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2029-30", "geography": "England", - "pe_missing": "CenTax's net fiscal cost adds intensive (bunching) and extensive (partners entering work) behavioural margins to the static cost; the dashboard publishes static costs and has no figure on this definition.", + "pe_missing": "CenTax's net fiscal cost covers the free hours alone and adds bunching (parents no longer holding income below \u00a3100,000) and partners entering work; the dashboard's net figure covers both schemes and models only the labour-supply response, not bunching.", "program": "free_childcare_hours", + "range_group": "centax_net_fy2029_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "+160m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": 160000000.0, @@ -26308,34 +26495,34 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch", - "ratio": -3.15625, - "delta": -665000000.0 + "ratio": -3.475, + "delta": -716000000.0 }, "results": [ { - "value": -505000000.0, + "value": -556000000.0, "status": "concept_mismatch", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "centax-net-vs-dashboard-dynamic: CenTax's net fiscal cost is for the free hours alone and adds two responses: parents who stop holding income below \u00a3100,000 (bunching) and partners who enter work (Table 4.2 and notes). The dashboard's net figure covers both schemes and models the labour-supply response only; it lists bunching as not modelled (results.json labour_supply.not_modelled). The two measure different things; the like-for-like comparison is CenTax's static cost." ], "baseline": "current_law", "status_effective": "concept_mismatch" @@ -26344,7 +26531,7 @@ "diagnosis": null }, { - "claim_id": "2ad7d14489e0a073fb55", + "claim_id": "53c7008af6b8a702630a", "source": "centax", "country": "UK", "source_column": "Low-Cost Scenario, tax year 2030: Static Cost (\u00a3)", @@ -26367,13 +26554,16 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", + "estimate_kind": "range_high", "fy": "2029-30", "geography": "England", "program": "free_childcare_hours", + "range_group": "centax_static_fy2029_scenarios", "scenario": "low_cost", "sign_convention": "positive = yield to the Exchequer (Table 4.2 notes: 'negative = cost to the Exchequer')", "value_verbatim": "-700m" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "db11caf3e07ba4d1", "external_value": -700000000.0, @@ -26388,11 +26578,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed", @@ -26406,11 +26596,11 @@ "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"thirty_hours_static_fy2029\", \"field\": \"budget.gross_bn.thirty_hours.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "hours-draw-microcosm-1126: The dashboard's Microcosm release draws each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what fuller use implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage), and its rerun on the first release with the corrected draw (uk-childcare-100k-threshold#8, an uncertified release) gives \u00a31.009bn and \u00a31.215bn for the 30 hours, 26% and 24% above CenTax's central static cost." + "hours-draw-microcosm-1126: The published run's Microcosm release drew each family's use of the extended hours with a mean of about 15 of the 30 weekly hours (microcosm#1126, which derives DfE's mean as about 28.5), so the static cost of the 30 hours is about half what CenTax's take-up implies. The dashboard's own full-use sensitivity adds \u00a30.60bn in 2027-28 and \u00a30.70bn in 2029-30 (results.json budget.sensitivities.effects_bn.full_30_hour_usage). Reruns on later, uncertified releases with the corrected draw (uk-childcare-100k-threshold#8, 22 October 2026) put the 30-hours cost at \u00a30.96bn in 2027-28 and \u00a31.12bn in 2029-30 as shipped, and \u00a30.87bn and \u00a30.96bn on CenTax's basis (no hours for reception children, DfE's funding rates, pension contributions deducted from income, 9-11-month-olds added)." ], "baseline": "current_law", "status_effective": "constructed" @@ -27377,7 +27567,7 @@ "diagnosis": null }, { - "claim_id": "eef80e8be4769cc2ce16", + "claim_id": "3e0f4ed92a011b5a1d9a", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2026/27", @@ -27396,64 +27586,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2026", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2026-27", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.330" + "value_verbatim": "0.070" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 330000000.0, + "external_value": 70000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 330000000.0, + "external_value_pe_sign": 70000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.690909090909091, - "delta": -888000000.0 + "status_effective": "comparable", + "ratio": -6.442857142857143, + "delta": -521000000.0 }, "results": [ { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "f40fe6519a80e65479f2", + "claim_id": "9def514bdb6ddd5f7abe", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2026/27", @@ -27472,64 +27662,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2026", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2026", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2026-27", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.070" + "value_verbatim": "0.330" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 70000000.0, + "external_value": 330000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 70000000.0, + "external_value_pe_sign": 330000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.9714285714285715, - "delta": -628000000.0 + "status_effective": "comparable", + "ratio": -1.3666666666666667, + "delta": -781000000.0 }, "results": [ { - "value": -558000000.0, - "status": "concept_mismatch", + "value": -451000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2026\", \"field\": \"budget.gross_bn.total.2026\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2026\", \"field\": \"labour_supply.dynamic_cost_bn.central.2026\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "123f8c0626dfc2a46e5d", + "claim_id": "b4b2459d4a9c757a940f", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2027/28", @@ -27548,64 +27738,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2027", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2027-28", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.349" + "value_verbatim": "0.077" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 349000000.0, + "external_value": 77000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 349000000.0, + "external_value_pe_sign": 77000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.6991404011461317, - "delta": -942000000.0 + "status_effective": "comparable", + "ratio": -6.220779220779221, + "delta": -556000000.0 }, "results": [ { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "ba4937e8a058012d9f58", + "claim_id": "deadc9241b4fd49010e2", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2027/28", @@ -27624,64 +27814,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2027", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2027", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2027-28", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.077" + "value_verbatim": "0.349" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 77000000.0, + "external_value": 349000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 77000000.0, + "external_value_pe_sign": 349000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.701298701298701, - "delta": -670000000.0 + "status_effective": "comparable", + "ratio": -1.3724928366762177, + "delta": -828000000.0 }, "results": [ { - "value": -593000000.0, - "status": "concept_mismatch", + "value": -479000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2027\", \"field\": \"budget.gross_bn.total.2027\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2027\", \"field\": \"labour_supply.dynamic_cost_bn.central.2027\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "985648c9838b5184dc10", + "claim_id": "9af08733f1c6826235aa", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2028/29", @@ -27700,64 +27890,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table3_fy2028", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table2_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2028-29", - "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_pension_exogenous_labour", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.399" + "value_verbatim": "0.091" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 399000000.0, + "external_value": 91000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 399000000.0, + "external_value_pe_sign": 91000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.6340852130325814, - "delta": -1051000000.0 + "status_effective": "comparable", + "ratio": -5.857142857142857, + "delta": -624000000.0 }, "results": [ { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "d6f07da73635a2c1e185", + "claim_id": "bf09bd19074e21cabac6", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2028/29", @@ -27776,64 +27966,64 @@ "program": "free_childcare_hours_and_tax_free_childcare", "conditions": { "analysis": "uk/childcare-100k-threshold", - "analysis_claim": "cbp_net_table2_fy2028", - "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "analysis_claim": "cbp_net_table3_fy2028", + "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2028-29", - "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", + "model": "endogenous_labour_no_pension", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", - "value_verbatim": "0.091" + "value_verbatim": "0.399" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", - "external_value": 91000000.0, + "external_value": 399000000.0, "sign_relative_to_pe": "same", "sign_basis": "stated:exchequer:same", - "external_value_pe_sign": 91000000.0, + "external_value_pe_sign": 399000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -7.164835164835165, - "delta": -743000000.0 + "status_effective": "comparable", + "ratio": -1.3358395989974938, + "delta": -932000000.0 }, "results": [ { - "value": -652000000.0, - "status": "concept_mismatch", + "value": -533000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2028\", \"field\": \"budget.gross_bn.total.2028\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2028\", \"field\": \"labour_supply.dynamic_cost_bn.central.2028\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "3a5f40190cc9637435d5", + "claim_id": "6587454d24d586b6fdf8", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2029/30", @@ -27854,16 +28044,16 @@ "analysis": "uk/childcare-100k-threshold", "analysis_claim": "cbp_net_table3_fy2029", "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000; Frisch elasticity 0.3", - "basis": "net of behavioural responses", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2029-30", "model": "endogenous_labour_no_pension", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", "value_verbatim": "0.427" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", "external_value": 427000000.0, @@ -27873,43 +28063,43 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -1.5878220140515222, - "delta": -1105000000.0 + "status_effective": "comparable", + "ratio": -1.3021077283372364, + "delta": -983000000.0 }, "results": [ { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null }, { - "claim_id": "68b30471cf9f5a87368c", + "claim_id": "c7aee9e51cd5570028cc", "source": "centre_for_british_progress", "country": "UK", "source_column": "Abolish Cliff, 2029/30", @@ -27930,16 +28120,16 @@ "analysis": "uk/childcare-100k-threshold", "analysis_claim": "cbp_net_table2_fy2029", "assumptions": "full utilisation of FCWP and \u00a3500 utilisation of TFC; friction parameter \u00a31000", - "basis": "net of behavioural responses", + "basis": "post_behavioural", "benchmark_class": "different_model", "country": "UK", "fy": "2029-30", "model": "endogenous_pension_exogenous_labour", - "pe_missing": "CBP's net fiscal impact includes pension-contribution (Table 2) or labour-supply (Table 3) responses and is reported as the net gain to the Exchequer; the dashboard publishes static costs and has no figure on this definition.", "program": "free_childcare_hours_and_tax_free_childcare", "sign_convention": "positive = yield to the Exchequer (the tables report the projected net fiscal impact)", "value_verbatim": "0.132" }, + "measure": null, "reform_framework": "policy_ref", "reform_key": "6afa9ff6f3c2b955", "external_value": 132000000.0, @@ -27949,37 +28139,37 @@ "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch", - "ratio": -5.136363636363637, - "delta": -810000000.0 + "status_effective": "comparable", + "ratio": -4.212121212121212, + "delta": -688000000.0 }, "results": [ { - "value": -678000000.0, - "status": "concept_mismatch", + "value": -556000000.0, + "status": "comparable", "engine_version": "policyengine==6.2.1 policyengine_uk==2.102.3", "data_bundle": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68", "release": "1002T230158Z", - "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": false, \"estimate\": \"both_schemes_static_fy2029\", \"field\": \"budget.gross_bn.total.2029\", \"path\": \"data/results.json\", \"relation\": null, \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\"}", + "construction": "{\"analysis\": \"uk/childcare-100k-threshold\", \"commit\": \"f25f0dbd46c31d336bde06898d801a1887a8cf89\", \"compared\": true, \"estimate\": \"both_schemes_dynamic_central_fy2029\", \"field\": \"labour_supply.dynamic_cost_bn.central.2029\", \"path\": \"data/results.json\", \"relation\": \"same\", \"repo\": \"PolicyEngine/uk-childcare-100k-threshold\", \"sha256\": \"4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a\", \"superseded_on\": null}", "policyengine_variables": [], "computed_at": "2026-10-09T11:50:21+00:00", "annotations": [ - "net-of-behaviour-vs-static: CenTax's net fiscal cost and the Centre for British Progress's net fiscal impact include behavioural responses and the tax they raise: CenTax adds parents no longer holding income below \u00a3100,000 and partners entering work (Table 4.2); CBP models pension contributions (Table 2) or labour supply (Table 3). The dashboard's figures beside them are static gross costs, so the pairs measure different things. The like-for-like comparison is CenTax's static cost, annotated above." + "cbp-bunching-vs-labour-supply: Both figures are the net fiscal effect of removing both limits after behavioural responses, and they have opposite signs: CBP finds a net gain to the Exchequer, the dashboard a net cost. CBP's gain comes from parents who stop holding income below \u00a3100,000, through pension contributions (Table 2) or hours worked (Table 3). The dashboard does not model that bunching response (results.json labour_supply.not_modelled) and starts from a static cost that the hours draw understates (microcosm#1126)." ], "baseline": "current_law", - "status_effective": "concept_mismatch" + "status_effective": "comparable" } ], "diagnosis": null diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index cbd7075ab..7826090b5 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -206,7 +206,10 @@ (re.compile(r"^\s*(?:tn|trillion)\b", re.I), "tn"), (re.compile(r"^\s*(?:m|mn|million)\b", re.I), "m"), (re.compile(r"^\s*(?:k|thousand)\b", re.I), "k"), - (re.compile(r"^\s*(?:%|per\s?cent\b|percent\b|pp\b|percentage points?\b)", re.I), "pct"), + ( + re.compile(r"^\s*(?:%|per\s?cent\b|percent\b|pp\b|percentage points?\b)", re.I), + "pct", + ), ) _NUMBER = re.compile(r"\d+(?:,\d{3})*(?:\.\d+)?|\.\d+") @@ -449,10 +452,16 @@ def load_policy(path: Path = POLICY_PATH) -> dict: raise ValueError( f"{path}: gap_threshold must be a number between 0 and 1: {threshold!r}" ) - for key in ("promotion_window_days", "reharvest_window_days"): + # PolicyEngine/newsletters refuses an index whose window is outside + # 1-90 days, so a mistyped window cannot make an old search count + for key, most in (("promotion_window_days", 90), ("reharvest_window_days", 366)): days = policy.get(key) - if not (isinstance(days, int) and not isinstance(days, bool) and days > 0): - raise ValueError(f"{path}: {key} must be a positive whole number: {days!r}") + if not ( + isinstance(days, int) and not isinstance(days, bool) and 0 < days <= most + ): + raise ValueError( + f"{path}: {key} must be a whole number of days from 1 to {most}: {days!r}" + ) return policy @@ -549,7 +558,9 @@ def load_analysis(directory: Path) -> AnalysisRecord: continue try: parts[name] = ( - _read_jsonl(path) if name.endswith(".jsonl") else _loads(path.read_text()) + _read_jsonl(path) + if name.endswith(".jsonl") + else _loads(path.read_text()) ) except (ValueError, json.JSONDecodeError) as exc: errors.append(f"{directory.name}: {name} does not parse: {exc}") @@ -647,7 +658,9 @@ def _check_basis(basis: dict, where: str) -> list[str]: def _check_bases(bases, where: str) -> list[str]: if not (isinstance(bases, list) and bases): - return [f"{where}: basis must cite at least one source (no fabricated mechanisms)"] + return [ + f"{where}: basis must cite at least one source (no fabricated mechanisms)" + ] errors = [] for b in bases: errors.extend(_check_basis(b if isinstance(b, dict) else {}, where)) @@ -669,7 +682,9 @@ def _check_identity(row: dict, where: str) -> list[str]: if fy is not None: match = _FY.match(fy) if not match or (int(match.group(1)) + 1) % 100 != int(match.group(2)): - errors.append(f"{where}: conditions.fy {fy!r} is not a financial year like 2027-28") + errors.append( + f"{where}: conditions.fy {fy!r} is not a financial year like 2027-28" + ) else: end = int(match.group(1)) + 1 if row.get("period") != end: @@ -690,7 +705,9 @@ def _check_estimate(e: dict, where: str, world_keys: set) -> list[str]: if not _registered(Metric, e.get("metric")): errors.append(f"{where}: metric {e.get('metric')!r} is not registered") if not _registered(UnitConcept, e.get("unit_concept")): - errors.append(f"{where}: unit_concept {e.get('unit_concept')!r} is not registered") + errors.append( + f"{where}: unit_concept {e.get('unit_concept')!r} is not registered" + ) if not _registered(TimeBasis, e.get("time_basis")): errors.append(f"{where}: time_basis {e.get('time_basis')!r} is not registered") if type(e.get("period")) is not int: @@ -707,7 +724,10 @@ def _check_estimate(e: dict, where: str, world_keys: set) -> list[str]: for f in ("path", "field", "dataset"): if not _nonempty_str(prov.get(f)): errors.append(f"{where}: provenance.{f} is empty") - if not (isinstance(prov.get("computed_at"), str) and _DATETIME.match(prov["computed_at"])): + if not ( + isinstance(prov.get("computed_at"), str) + and _DATETIME.match(prov["computed_at"]) + ): errors.append(f"{where}: provenance.computed_at must be an ISO date or time") engine = prov.get("engine") if not (_string_map(engine) and engine): @@ -717,7 +737,8 @@ def _check_estimate(e: dict, where: str, world_keys: set) -> list[str]: def _same_quantity(claim: dict, estimate: dict) -> bool: return all(claim.get(f) == estimate.get(f) for f in IDENTITY_FIELDS) and all( - (claim.get("conditions") or {}).get(f) == (estimate.get("conditions") or {}).get(f) + (claim.get("conditions") or {}).get(f) + == (estimate.get("conditions") or {}).get(f) for f in IDENTITY_CONDITIONS ) @@ -790,7 +811,9 @@ def validate( errors.append(f"{name}: owner.author must be the app-v2 authors.json key") published = a.get("published") if published is not None and not (_is_date(published) and published <= today): - errors.append(f"{name}: published must be a real date no later than today, or null") + errors.append( + f"{name}: published must be a real date no later than today, or null" + ) published = None worlds = a.get("worlds") if not isinstance(worlds, dict): @@ -815,7 +838,9 @@ def validate( for i, e in enumerate(a.get("pe_estimates") or []): key = e.get("key") if not (isinstance(key, str) and _KEY.match(key)) or key in estimates: - errors.append(f"{name}: pe_estimates[{i}]: key {key!r} missing, malformed or repeated") + errors.append( + f"{name}: pe_estimates[{i}]: key {key!r} missing, malformed or repeated" + ) continue estimates[key] = e errors.extend(_check_estimate(e, f"{name}: estimate {key}", world_keys)) @@ -830,11 +855,17 @@ def validate( if e.get("key") not in estimates: errors.append(f"{where}: key {e.get('key')!r} is not a current estimate's") if not (_is_date(e.get("superseded_on")) and e["superseded_on"] <= today): - errors.append(f"{where}: superseded_on must be a real date no later than today") + errors.append( + f"{where}: superseded_on must be a real date no later than today" + ) errors.extend(_check_estimate(e, where, world_keys)) if not any(err.startswith(where) and "provenance" in err for err in errors): superseded_pins.add(pin(e["provenance"])) - pins_now = set(current_pins(a)) if not any("provenance" in err for err in errors) else set() + pins_now = ( + set(current_pins(a)) + if not any("provenance" in err for err in errors) + else set() + ) # manifest ---------------------------------------------------------------- manifest: dict[str, dict] = {} @@ -872,7 +903,9 @@ def validate( if date > today: errors.append(f"{where}: dated {date}, after today ({today})") if date < earliest: - errors.append(f"{where}: dated {date}, before the {country} producer list ({earliest})") + errors.append( + f"{where}: dated {date}, before the {country} producer list ({earliest})" + ) if date < previous_date: errors.append(f"{where}: searches must be in date order") previous_date = date @@ -881,7 +914,9 @@ def validate( if i == 0: first_date = date if skind not in FIRST_KINDS: - errors.append(f"{where}: the first search is a prepublish or backfill search") + errors.append( + f"{where}: the first search is a prepublish or backfill search" + ) elif skind in FIRST_KINDS: errors.append(f"{where}: only the first search is {skind}") if skind == "prepublish" and published is not None and date > published: @@ -890,16 +925,26 @@ def validate( ) if skind == "backfill": if published is None: - errors.append(f"{where}: a backfill search needs analysis.json published") + errors.append( + f"{where}: a backfill search needs analysis.json published" + ) elif date < published: - errors.append(f"{where}: a backfill search is dated after publication ({published})") + errors.append( + f"{where}: a backfill search is dated after publication ({published})" + ) if skind in LATER_KINDS: if published is None: - errors.append(f"{where}: a {skind} search needs analysis.json published") + errors.append( + f"{where}: a {skind} search needs analysis.json published" + ) elif date <= published: - errors.append(f"{where}: a {skind} search is dated after publication ({published})") + errors.append( + f"{where}: a {skind} search is dated after publication ({published})" + ) if first_date is not None and date <= first_date: - errors.append(f"{where}: a {skind} search is dated after the first search ({first_date})") + errors.append( + f"{where}: a {skind} search is dated after the first search ({first_date})" + ) if not _nonempty_str(s.get("by")): errors.append(f"{where}: by must name the session or Subfleet job") pins = s.get("results_pins") @@ -909,7 +954,9 @@ def validate( "(repo@commit:sha256; `comparables pins` prints them)" ) elif pins_now and not set(pins) <= pins_now | superseded_pins: - errors.append(f"{where}: results_pins name a results file no estimate is read from") + errors.append( + f"{where}: results_pins name a results file no estimate is read from" + ) entries = s.get("producers") if not isinstance(entries, list): errors.append(f"{where}: producers must be a list") @@ -922,7 +969,9 @@ def validate( if missing: errors.append(f"{where}: producers not searched: {missing}") if unknown: - errors.append(f"{where}: producers not on the {country} list on {date}: {unknown}") + errors.append( + f"{where}: producers not on the {country} list on {date}: {unknown}" + ) if len(logged) != len(set(logged)): errors.append(f"{where}: a producer is logged twice") by_slug = {p["slug"]: p for p in required} @@ -936,8 +985,12 @@ def validate( claims = entry.get("claims", []) queries = entry.get("queries", []) checked = entry.get("checked", []) - if not (_string_list(claims) and _string_list(queries) and _string_list(checked)): - errors.append(f"{pwhere}: claims, queries and checked are lists of strings") + if not ( + _string_list(claims) and _string_list(queries) and _string_list(checked) + ): + errors.append( + f"{pwhere}: claims, queries and checked are lists of strings" + ) continue if result not in RESULTS: errors.append(f"{pwhere}: result {result!r} not in {RESULTS}") @@ -957,14 +1010,18 @@ def validate( errors.append(f"{pwhere}: not_applicable with claims") continue if not (queries or checked): - errors.append(f"{pwhere}: {result} with no query run and no page checked") + errors.append( + f"{pwhere}: {result} with no query run and no page checked" + ) if result == "none_found" and claims: errors.append(f"{pwhere}: none_found with claims") if result == "found_not_staged": if claims: errors.append(f"{pwhere}: found_not_staged with claims") if not checked: - errors.append(f"{pwhere}: found_not_staged lists the pages it was seen on") + errors.append( + f"{pwhere}: found_not_staged lists the pages it was seen on" + ) if not _nonempty_str(entry.get("note"), 20): errors.append( f"{pwhere}: found_not_staged says what was found and why it is not staged" @@ -973,7 +1030,9 @@ def validate( errors.append(f"{pwhere}: found with no claims") for key in claims: if key in claim_found: - errors.append(f"{pwhere}: claim {key} already found by an earlier search") + errors.append( + f"{pwhere}: claim {key} already found by an earlier search" + ) claim_found[key] = (date, skind, slug_) if searches and pins_now: latest = searches[-1].get("results_pins") @@ -1001,9 +1060,13 @@ def validate( errors.append(f"{where}: no search entry lists this claim") else: if c.get("found_on") != found[0]: - errors.append(f"{where}: found_on must be {found[0]}, the search that found it") + errors.append( + f"{where}: found_on must be {found[0]}, the search that found it" + ) if c.get("found_by") != found[2]: - errors.append(f"{where}: found_by must be {found[2]}, the producer logged") + errors.append( + f"{where}: found_by must be {found[2]}, the producer logged" + ) source = c.get("source") found_by = c.get("found_by") if not (isinstance(source, str) and _SLUG.match(source)): @@ -1030,13 +1093,17 @@ def validate( if not _nonempty_str(c.get("proposed_metric")): errors.append(f"{where}: metric null needs proposed_metric") elif not _registered(Metric, metric): - errors.append(f"{where}: metric {metric!r} is not registered; use proposed_metric") + errors.append( + f"{where}: metric {metric!r} is not registered; use proposed_metric" + ) unit = c.get("unit_concept") if unit is None: if not _nonempty_str(c.get("proposed_unit")): errors.append(f"{where}: unit_concept null needs proposed_unit") elif not _registered(UnitConcept, unit): - errors.append(f"{where}: unit_concept {unit!r} is not registered; use proposed_unit") + errors.append( + f"{where}: unit_concept {unit!r} is not registered; use proposed_unit" + ) value_kind = c.get("value_kind") if value_kind not in VALUE_KINDS: errors.append(f"{where}: value_kind {value_kind!r} not in {VALUE_KINDS}") @@ -1052,20 +1119,32 @@ def validate( if type(c.get("period")) is not int: errors.append(f"{where}: period must be an integer year (FY end year)") if not _registered(TimeBasis, c.get("time_basis")): - errors.append(f"{where}: time_basis {c.get('time_basis')!r} is not registered") + errors.append( + f"{where}: time_basis {c.get('time_basis')!r} is not registered" + ) errors.extend(_check_identity(c, where)) world = c.get("world") if world not in world_keys: errors.append(f"{where}: world {world!r} is not defined in analysis.json") if world != BASELINE_WORLD and not _nonempty_str(c.get("reform_hint")): - errors.append(f"{where}: reform_hint must quote the producer's own words for the world") + errors.append( + f"{where}: reform_hint must quote the producer's own words for the world" + ) if c.get("attribution") not in ATTRIBUTIONS: - errors.append(f"{where}: attribution {c.get('attribution')!r} not in {ATTRIBUTIONS}") + errors.append( + f"{where}: attribution {c.get('attribution')!r} not in {ATTRIBUTIONS}" + ) bclass = c.get("benchmark_class") if bclass not in BENCHMARK_CLASSES: - errors.append(f"{where}: benchmark_class {bclass!r} not in {BENCHMARK_CLASSES}") - if (c.get("attribution") == "same_assumptions") != (bclass == "same_assumptions"): - errors.append(f"{where}: same_assumptions attribution and benchmark_class go together") + errors.append( + f"{where}: benchmark_class {bclass!r} not in {BENCHMARK_CLASSES}" + ) + if (c.get("attribution") == "same_assumptions") != ( + bclass == "same_assumptions" + ): + errors.append( + f"{where}: same_assumptions attribution and benchmark_class go together" + ) if bclass == "administrative_fact" and world == BASELINE_WORLD: errors.append( f"{where}: an administrative level is calibration material; stage it " @@ -1078,7 +1157,9 @@ def validate( "scorecard_db/baselines.py (harvest rule 7: register it first)" ) if "proposed_baseline" in c: - errors.append(f"{where}: register the baseline before staging (harvest rule 7)") + errors.append( + f"{where}: register the baseline before staging (harvest rule 7)" + ) pub = c.get("publication") or {} for f in ("title", "url", "publisher", "locator", "access"): if not _nonempty_str(pub.get(f)): @@ -1086,14 +1167,20 @@ def validate( if not _is_date(pub.get("date")): errors.append(f"{where}: publication.date must be a real YYYY-MM-DD date") elif _is_date(c.get("found_on")) and pub["date"] > c["found_on"]: - errors.append(f"{where}: published {pub['date']}, after it was found ({c['found_on']})") + errors.append( + f"{where}: published {pub['date']}, after it was found ({c['found_on']})" + ) sha = pub.get("primary_sha256") if sha not in manifest: - errors.append(f"{where}: publication.primary_sha256 is not in manifest.jsonl") + errors.append( + f"{where}: publication.primary_sha256 is not in manifest.jsonl" + ) else: used_primaries.add(sha) if manifest[sha].get("url") != pub.get("url"): - errors.append(f"{where}: publication.url differs from its manifest line") + errors.append( + f"{where}: publication.url differs from its manifest line" + ) for f in ("source_table", "source_column", "quote"): if not _nonempty_str(c.get(f)): errors.append(f"{where}: {f} is empty") @@ -1104,7 +1191,9 @@ def validate( if value_kind in RANGE_KINDS: group = (c.get("conditions") or {}).get("range_group") if not (isinstance(group, str) and _SLUG.match(group)): - errors.append(f"{where}: a range bound names its range_group in conditions") + errors.append( + f"{where}: a range bound names its range_group in conditions" + ) else: range_groups[group].append(c) compare = c.get("compare_to") @@ -1121,7 +1210,9 @@ def validate( "nearest_estimate and annotate why they differ" ) elif not near and nearest is not None: - errors.append(f"{where}: nearest_estimate {nearest!r} is not near this claim") + errors.append( + f"{where}: nearest_estimate {nearest!r} is not near this claim" + ) elif near: must_annotate.append(key) est = estimates[nearest] @@ -1149,9 +1240,13 @@ def validate( continue compared_keys.append(key) if c.get("attribution") != "own": - errors.append(f"{where}: only a producer's own estimate is compared ({c.get('attribution')})") + errors.append( + f"{where}: only a producer's own estimate is compared ({c.get('attribution')})" + ) if value_kind == "cumulative": - errors.append(f"{where}: a cumulative figure is not compared with a single-year estimate") + errors.append( + f"{where}: a cumulative figure is not compared with a single-year estimate" + ) if c.get("world") != est.get("world"): across = compare.get("across_worlds") if not isinstance(across, dict): @@ -1162,7 +1257,9 @@ def validate( ) else: if not _nonempty_str(across.get("reason"), 20): - errors.append(f"{where}: across_worlds.reason must say why the quantities match") + errors.append( + f"{where}: across_worlds.reason must say why the quantities match" + ) errors.extend(_check_bases(across.get("basis"), where)) for f in ("metric", "unit_concept", "period", "time_basis"): if c.get(f) != est.get(f): @@ -1199,19 +1296,34 @@ def validate( errors.append(f"{name}: search lists claim {key}, which is not staged") for sha, m in manifest.items(): if sha not in used_primaries: - errors.append(f"{name}: manifest line for {m.get('url')} is cited by no claim") + errors.append( + f"{name}: manifest line for {m.get('url')} is cited by no claim" + ) for group, members in range_groups.items(): kinds = sorted(m.get("value_kind") for m in members) if kinds != ["range_high", "range_low"]: - errors.append(f"{name}: range_group {group} needs exactly one range_low and one range_high") + errors.append( + f"{name}: range_group {group} needs exactly one range_low and one range_high" + ) continue low, high = members - shared = ("source", "metric", "unit_concept", "period", "time_basis", "world", "compare_to") + shared = ( + "source", + "metric", + "unit_concept", + "period", + "time_basis", + "world", + "compare_to", + ) if any(low.get(f) != high.get(f) for f in shared) or any( - (low.get("conditions") or {}).get(f) != (high.get("conditions") or {}).get(f) + (low.get("conditions") or {}).get(f) + != (high.get("conditions") or {}).get(f) for f in IDENTITY_CONDITIONS ): - errors.append(f"{name}: range_group {group}'s bounds measure different things") + errors.append( + f"{name}: range_group {group}'s bounds measure different things" + ) # annotations ------------------------------------------------------------------- annotated: dict[str, dict] = {} @@ -1233,9 +1345,16 @@ def validate( errors.append(f"{where}: explains_gap must be true or false") if status == "pending": if n.get("explains_gap"): - errors.append(f"{where}: a pending annotation does not explain the gap yet") - if not (isinstance(n.get("tracking_issue"), str) and _GITHUB_ISSUE.match(n["tracking_issue"])): - errors.append(f"{where}: pending needs the tracking_issue opened for the owner") + errors.append( + f"{where}: a pending annotation does not explain the gap yet" + ) + if not ( + isinstance(n.get("tracking_issue"), str) + and _GITHUB_ISSUE.match(n["tracking_issue"]) + ): + errors.append( + f"{where}: pending needs the tracking_issue opened for the owner" + ) keys = n.get("claims") if not (isinstance(keys, list) and keys): errors.append(f"{where}: claims must list the claim keys it covers") @@ -1245,7 +1364,9 @@ def validate( errors.append(f"{where}: claim {key} is not staged") continue if key in annotated: - errors.append(f"{where}: claim {key} already has a status from {annotated[key]['id']}") + errors.append( + f"{where}: claim {key} already has a status from {annotated[key]['id']}" + ) continue annotated[key] = n c = claims_by_key[key] @@ -1256,7 +1377,9 @@ def validate( f"{COMPARED_STATUSES}; a concept mismatch is not compared" ) est = estimates[c["compare_to"]["pe_estimate"]] - if status == ComparisonStatus.COMPARABLE.value and c.get("world") != est.get("world"): + if status == ComparisonStatus.COMPARABLE.value and c.get( + "world" + ) != est.get("world"): errors.append( f"{where}: {key} is compared across worlds, so it is constructed, " "not comparable" @@ -1265,7 +1388,10 @@ def validate( errors.append( f"{where}: {key} is not compared, so its status is one of {MISMATCH_STATUSES}" ) - if status == "pending" and claim_found.get(key, ("", ""))[1] not in POST_PUBLICATION_KINDS: + if ( + status == "pending" + and claim_found.get(key, ("", ""))[1] not in POST_PUBLICATION_KINDS + ): errors.append( f"{where}: {key} was found before publication; its gap is " "explained before the analysis is listed, not left pending" @@ -1442,8 +1568,12 @@ def index_entry(record: AnalysisRecord, policy: dict) -> dict: "explained": sum(1 for r in over if r["explains_gap"]), "pending": sum(1 for r in rows if r["status"] == "pending"), "unbounded_gaps": sum(1 for g in gaps if math.isinf(g)), - "max_abs_gap": _round(max((g for g in gaps if not math.isinf(g)), default=None)), - "tracking_issues": sorted({r["tracking_issue"] for r in rows if r["tracking_issue"]}), + "max_abs_gap": _round( + max((g for g in gaps if not math.isinf(g)), default=None) + ), + "tracking_issues": sorted( + {r["tracking_issue"] for r in rows if r["tracking_issue"]} + ), "search_log": f"{REPO_URL}/blob/main/sources/{record.name}/search_log.json", "comparisons": _comparisons(record, rows), } @@ -1531,6 +1661,16 @@ def build_index( "schema": INDEX_SCHEMA, "gap_threshold": policy["gap_threshold"], "promotion_window_days": policy["promotion_window_days"], + # The rules for which policyengine.org entries need a record, so the + # sites that check entries against this index can check they apply + # the same ones (research_entries, may_declare_no_estimate). + "entry_rules": { + "analysis_tags": sorted(ANALYSIS_TAGS), + "no_estimate_tags": sorted(NO_ESTIMATE_TAGS), + "country_tags": list(COUNTRY_TAGS), + "default_country": DEFAULT_COUNTRY, + "search_kinds": list(SEARCH_KINDS), + }, "analyses": analyses, } @@ -1549,7 +1689,9 @@ def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: if search_date: searches = [s for s in searches if s["date"] == search_date] if not searches: - raise ValueError(f"{record.name}: no prepublish or backfill search on {search_date}") + raise ValueError( + f"{record.name}: no prepublish or backfill search on {search_date}" + ) summary = _search_summary(searches[-1]) return { "status": "reviewed", @@ -1565,7 +1707,9 @@ def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: # --- scaffolding ----------------------------------------------------------------- -def scaffold(country: str, kind: str, slug: str, today: str, root: Path = SOURCES) -> Path: +def scaffold( + country: str, kind: str, slug: str, today: str, root: Path = SOURCES +) -> Path: """A skeleton directory for a new search, with every producer to log.""" producers = load_producers() country = country.upper() @@ -1575,7 +1719,9 @@ def scaffold(country: str, kind: str, slug: str, today: str, root: Path = SOURCE ) directory = root / f"{DIR_PREFIX}{country.lower()}-{slug}" if directory.exists(): - raise SystemExit(f"{directory} exists; add a search to its search_log.json instead") + raise SystemExit( + f"{directory} exists; add a search to its search_log.json instead" + ) directory.mkdir(parents=True) analysis = { "schema": ANALYSIS_SCHEMA, @@ -1614,8 +1760,12 @@ def scaffold(country: str, kind: str, slug: str, today: str, root: Path = SOURCE } ], } - (directory / "analysis.json").write_text(json.dumps(analysis, indent=1, ensure_ascii=False) + "\n") - (directory / "search_log.json").write_text(json.dumps(log, indent=1, ensure_ascii=False) + "\n") + (directory / "analysis.json").write_text( + json.dumps(analysis, indent=1, ensure_ascii=False) + "\n" + ) + (directory / "search_log.json").write_text( + json.dumps(log, indent=1, ensure_ascii=False) + "\n" + ) (directory / "manifest.jsonl").write_text("") (directory / "claims_staged.jsonl").write_text("") (directory / "annotations.json").write_text("[]\n") @@ -1686,7 +1836,9 @@ def publishes_estimate(entry: dict) -> bool: entry["kind"], entry["tags"] ): return False - return record.get("status") == "reviewed" or bool(set(entry["tags"]) & ANALYSIS_TAGS) + return record.get("status") == "reviewed" or bool( + set(entry["tags"]) & ANALYSIS_TAGS + ) def due( @@ -1702,15 +1854,27 @@ def due( rows = [] sources: dict[str, str] = {} for e in research_entries(apps, posts): - if not e["date"] or not (start <= e["date"] <= today) or not publishes_estimate(e): + if ( + not e["date"] + or not (start <= e["date"] <= today) + or not publishes_estimate(e) + ): continue record = e.get("comparables") or {} candidates = [f"{DIR_PREFIX}{c}-{e['slug']}" for c in e["country_ids"]] - name = record.get("scorecardSource") if record.get("status") == "reviewed" else None - name = name or next((n for n in candidates if (root / n).is_dir()), candidates[0]) + name = ( + record.get("scorecardSource") + if record.get("status") == "reviewed" + else None + ) + name = name or next( + (n for n in candidates if (root / n).is_dir()), candidates[0] + ) label = f"{e['kind']} {e['slug']}" if name in sources and sources[name] != label: - raise ValueError(f"{sources[name]} and {label} would share the record {name}") + raise ValueError( + f"{sources[name]} and {label} would share the record {name}" + ) sources[name] = label rows.append( { @@ -1734,7 +1898,9 @@ def fresh(entry: dict, on: str, window_days: int) -> list[str]: pins = sorted(entry.get("current_pins") or []) recent = [s for s in entry["searches"] if start <= s["date"] <= on] if not recent: - last = max((s["date"] for s in entry["searches"] if s["date"] <= on), default=None) + last = max( + (s["date"] for s in entry["searches"] if s["date"] <= on), default=None + ) reasons.append( f"no comparables search between {start} and {on} (last: {last or 'none'}); " "run a prepromotion search before the push" @@ -1798,9 +1964,13 @@ def _main(argv: list[str]) -> int: parser = argparse.ArgumentParser(prog="python -m scorecard_db.comparables") sub = parser.add_subparsers(dest="cmd", required=True) p_check = sub.add_parser("check", help="validate analysis directories") - p_check.add_argument("dirs", nargs="*", help="directories (default: every sources/analysis-*)") + p_check.add_argument( + "dirs", nargs="*", help="directories (default: every sources/analysis-*)" + ) p_index = sub.add_parser("index", help="write data/comparables/index.json") - p_index.add_argument("--check", action="store_true", help="fail if the committed index is stale") + p_index.add_argument( + "--check", action="store_true", help="fail if the committed index is stale" + ) p_scaffold = sub.add_parser("scaffold", help="start a record for a new analysis") p_scaffold.add_argument("--country", required=True) p_scaffold.add_argument("--kind", required=True, choices=("app", "post")) @@ -1841,7 +2011,9 @@ def _main(argv: list[str]) -> int: text = render_index(build_index()) if args.check: if not INDEX_PATH.exists() or INDEX_PATH.read_text() != text: - print(f"{INDEX_PATH} is stale: run python -m scorecard_db.comparables index") + print( + f"{INDEX_PATH} is stale: run python -m scorecard_db.comparables index" + ) return 1 print(f"{INDEX_PATH} is current") return 0 @@ -1863,12 +2035,25 @@ def _main(argv: list[str]) -> int: print("\n".join(errors), file=sys.stderr) return 1 if args.cmd == "record": - print(json.dumps(app_record(record, args.search_date), indent=2, ensure_ascii=False)) + print( + json.dumps( + app_record(record, args.search_date), indent=2, ensure_ascii=False + ) + ) return 0 if args.cmd == "report": - print(json.dumps(report(record, args.since, policy), indent=1, ensure_ascii=False, default=str)) + print( + json.dumps( + report(record, args.since, policy), + indent=1, + ensure_ascii=False, + default=str, + ) + ) return 0 - reasons = fresh(index_entry(record, policy), args.on, policy["promotion_window_days"]) + reasons = fresh( + index_entry(record, policy), args.on, policy["promotion_window_days"] + ) for reason in reasons: print(f"NOT FRESH {args.source}: {reason}") if not reasons: @@ -1878,7 +2063,11 @@ def _main(argv: list[str]) -> int: apps = _loads(Path(args.apps).read_text()) posts = _loads(Path(args.posts).read_text()) window = load_policy()["reharvest_window_days"] - print(json.dumps(due(apps, posts, args.today, window), indent=1, ensure_ascii=False)) + print( + json.dumps( + due(apps, posts, args.today, window), indent=1, ensure_ascii=False + ) + ) return 0 return 2 diff --git a/scorecard_db/ingest_analysis_comparables.py b/scorecard_db/ingest_analysis_comparables.py index b026939e5..165e92e43 100644 --- a/scorecard_db/ingest_analysis_comparables.py +++ b/scorecard_db/ingest_analysis_comparables.py @@ -240,9 +240,7 @@ def stage(root: Path = cmp.SOURCES) -> tuple[list[ExternalScore], list[PEResult] world_baseline = a["worlds"].get(est["world"], {}).get("baseline") executed = _baseline_descriptor(world_baseline) or CURRENT_LAW_DESCRIPTOR history = [ - e - for e in a.get("superseded_estimates") or [] - if e["key"] == est["key"] + e for e in a.get("superseded_estimates") or [] if e["key"] == est["key"] ] for version in [est, *history]: prov = version["provenance"] diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 1946e337b..61132c0f2 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -144,7 +144,10 @@ def synthetic( "subject": "The cost of a test reform", "search_terms": ["test reform cost"], "worlds": { - "test_reform": {"policy": "uk_test_reform", "description": "A test reform world"} + "test_reform": { + "policy": "uk_test_reform", + "description": "A test reform world", + } }, "pe_estimates": [ { @@ -220,7 +223,12 @@ def synthetic( "status": "comparable", "explains_gap": True, "cause": "A documented difference in the test populations explains it.", - "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/1"}], + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/1", + } + ], } ) return cmp.AnalysisRecord( @@ -280,7 +288,9 @@ def note(nid, keys, status, explains=False, **extra): "status": status, "explains_gap": explains, "cause": "a cause long enough to count as one", - "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/9"}], + "basis": [ + {"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/9"} + ], **extra, } @@ -322,6 +332,15 @@ def test_index_lists_every_record_and_only_records(): assert index["promotion_window_days"] == WINDOW +def test_the_index_publishes_the_entry_rules_the_validator_applies(): + rules = json.loads(cmp.INDEX_PATH.read_text())["entry_rules"] + assert set(rules["analysis_tags"]) == cmp.ANALYSIS_TAGS + assert set(rules["no_estimate_tags"]) == cmp.NO_ESTIMATE_TAGS + assert rules["country_tags"] == list(cmp.COUNTRY_TAGS) + assert rules["default_country"] == cmp.DEFAULT_COUNTRY + assert rules["search_kinds"] == list(cmp.SEARCH_KINDS) + + def test_the_index_is_strict_json(): with pytest.raises(ValueError): cmp.render_index({"x": float("nan")}) @@ -337,9 +356,13 @@ def test_max_producer_list_is_kept(): def test_every_producer_is_complete(): for country, producers in PRODUCERS["countries"].items(): for p in producers: - assert p["name"] and p["kind"] and p["search_hints"] and p["where_to_look"], p["slug"] + assert ( + p["name"] and p["kind"] and p["search_hints"] and p["where_to_look"] + ), p["slug"] if p["slug"] not in MAX_LIST[country] and p["slug"] != "open_search": - assert p.get("why"), f"{p['slug']} was added beyond Max's list without a reason" + assert p.get("why"), ( + f"{p['slug']} was added beyond Max's list without a reason" + ) def test_a_producer_cannot_be_added_in_the_future(tmp_path): @@ -352,7 +375,10 @@ def test_a_producer_cannot_be_added_in_the_future(tmp_path): def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): - assert cmp.producer_for_source(PRODUCERS, "US", "ca_fiscal_note") == "state_fiscal_note" + assert ( + cmp.producer_for_source(PRODUCERS, "US", "ca_fiscal_note") + == "state_fiscal_note" + ) assert cmp.producer_for_source(PRODUCERS, "UK", "obr_efo") == "obr" assert cmp.producer_for_source(PRODUCERS, "UK", "unlisted_tank") is None @@ -362,7 +388,9 @@ def test_state_fiscal_note_sources_belong_to_state_fiscal_note(): def test_childcare_gap_fails_the_gate_without_its_explanation(): record = childcare() - record.annotations = [n for n in record.annotations if n["id"] != "hours-draw-microcosm-1126"] + record.annotations = [ + n for n in record.annotations if n["id"] != "hours-draw-microcosm-1126" + ] unexplained = [e for e in errors_of(record) if "no annotation explains it" in e] # three central years, and the two scenario ranges assert len(unexplained) == 7 @@ -371,19 +399,35 @@ def test_childcare_gap_fails_the_gate_without_its_explanation(): def test_childcare_figures_are_what_the_published_files_imply(): rows = {r["key"]: r for r in cmp.evaluate(childcare(), POLICY)} - assert rows["centax_static_central_fy2027"]["gap"] == pytest.approx(0.434 / 0.80 - 1) - assert rows["centax_static_central_fy2029"]["gap"] == pytest.approx(0.505 / 0.98 - 1) - assert rows["centax_static_central_fy2028"]["gap"] == pytest.approx(0.484 / 0.88 - 1) + assert rows["centax_static_central_fy2027"]["gap"] == pytest.approx( + 0.434 / 0.80 - 1 + ) + assert rows["centax_static_central_fy2029"]["gap"] == pytest.approx( + 0.505 / 0.98 - 1 + ) + assert rows["centax_static_central_fy2028"]["gap"] == pytest.approx( + 0.484 / 0.88 - 1 + ) # the scenario range is over by its nearer bound, the low-cost scenario - assert rows["centax_static_low_cost_fy2027"]["group_gap"] == pytest.approx(0.434 / 0.58 - 1) + assert rows["centax_static_low_cost_fy2027"]["group_gap"] == pytest.approx( + 0.434 / 0.58 - 1 + ) # compared across two worlds, so constructed rather than comparable assert rows["centax_static_central_fy2027"]["status"] == "constructed" # net of behaviour beside the dashboard's net figure, labelled a mismatch - assert rows["centax_net_central_fy2027"]["nearest"] == "both_schemes_dynamic_central_fy2027" + assert ( + rows["centax_net_central_fy2027"]["nearest"] + == "both_schemes_dynamic_central_fy2027" + ) assert rows["centax_net_central_fy2027"]["status"] == "concept_mismatch" - assert rows["centax_net_central_fy2027"]["nearest_gap"] == pytest.approx(-0.479 / -0.520 - 1) + assert rows["centax_net_central_fy2027"]["nearest_gap"] == pytest.approx( + -0.479 / -0.520 - 1 + ) # CBP's net gain against the dashboard's net cost: compared, signs disagree - assert rows["cbp_net_table3_fy2027"]["estimate"] == "both_schemes_dynamic_central_fy2027" + assert ( + rows["cbp_net_table3_fy2027"]["estimate"] + == "both_schemes_dynamic_central_fy2027" + ) assert rows["cbp_net_table3_fy2027"]["sign_disagrees"] assert rows["cbp_net_table3_fy2027"]["over"] @@ -442,20 +486,31 @@ def _evade(fn): EVASIONS = { "null metric": lambda r: claim0(r).update( - metric=None, proposed_metric="cost", compare_to=None, pe_missing="a different metric entirely" + metric=None, + proposed_metric="cost", + compare_to=None, + pe_missing="a different metric entirely", ), "uncompared with a one-line reason": lambda r: claim0(r).update( compare_to=None, pe_missing="a different concept, says the stager" ), "time basis annual": lambda r: claim0(r).update(time_basis="annual"), "period moved, fy kept": lambda r: claim0(r).update(period=2031), - "basis relabelled to net": lambda r: claim0(r)["conditions"].update(basis="post_behavioural"), - "programme relabelled": lambda r: claim0(r)["conditions"].update(program="another_programme"), + "basis relabelled to net": lambda r: claim0(r)["conditions"].update( + basis="post_behavioural" + ), + "programme relabelled": lambda r: claim0(r)["conditions"].update( + program="another_programme" + ), "chart reading at a different value": lambda r: claim0(r).update( - value_kind="approx_chart_reading", value=-299.0, note="read off the chart by eye" + value_kind="approx_chart_reading", + value=-299.0, + note="read off the chart by eye", ), "NaN value": lambda r: claim0(r).update(value=float("nan")), - "unsigned magnitude": lambda r: claim0(r)["conditions"].update(change_direction="unsigned"), + "unsigned magnitude": lambda r: claim0(r)["conditions"].update( + change_direction="unsigned" + ), "two-sided convention": lambda r: claim0(r)["conditions"].update( sign_convention="positive = reduction in borrowing; negative = increase in borrowing" ), @@ -472,41 +527,74 @@ def _evade(fn): r.annotations.append(note("x", ["ext_cost_fy2027"], "concept_mismatch")), ), "placeholder basis": lambda r: r.annotations.append( - {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "#"}]} + { + **note("x", ["ext_cost_fy2027"], "comparable", explains=True), + "basis": [{"kind": "document", "ref": "#"}], + } ), "a directory as basis": lambda r: r.annotations.append( - {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "sources"}]} + { + **note("x", ["ext_cost_fy2027"], "comparable", explains=True), + "basis": [{"kind": "document", "ref": "sources"}], + } ), "a bare host as basis": lambda r: r.annotations.append( - {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "document", "ref": "https://example.com"}]} + { + **note("x", ["ext_cost_fy2027"], "comparable", explains=True), + "basis": [{"kind": "document", "ref": "https://example.com"}], + } ), "pending before publication": lambda r: r.annotations.append( - note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + note( + "x", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/9", + ) ), "same-day reharvest": lambda r: ( post_publication(r, later=SEARCH_DATE), r.annotations.append( - note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + note( + "x", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/9", + ) ), ), "future-dated reharvest": lambda r: ( post_publication(r, later="2099-12-31"), r.annotations.append( - note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + note( + "x", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/9", + ) ), ), "reharvest with no publication date": lambda r: ( post_publication(r), r.analysis.update(published=None), r.annotations.append( - note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/9") + note( + "x", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/9", + ) ), ), "logged under open_search": lambda r: ( claim0(r).update(found_by="open_search"), - r.search_log["searches"][0].update(producers=_producers("open_search", ["ext_cost_fy2027"])), + r.search_log["searches"][0].update( + producers=_producers("open_search", ["ext_cost_fy2027"]) + ), + ), + "scale word disagrees": lambda r: claim0(r).update( + value_raw="-100bn", quote="Cost -100bn" ), - "scale word disagrees": lambda r: claim0(r).update(value_raw="-100bn", quote="Cost -100bn"), } @@ -536,7 +624,9 @@ def f(r): def test_a_prepublish_search_comes_before_publication(): errors = mutate(lambda r: r.analysis.update(published="2026-10-20")) - assert any("a prepublish search is dated on or before publication" in e for e in errors) + assert any( + "a prepublish search is dated on or before publication" in e for e in errors + ) def test_a_backfill_needs_and_follows_publication(): @@ -589,7 +679,12 @@ def f(r): record = synthetic(-100.0, -300.0) f(record) record.annotations.append( - note("x", ["ext_cost_fy2027"], "pending", tracking_issue="https://github.com/PolicyEngine/test/issues/4") + note( + "x", + ["ext_cost_fy2027"], + "pending", + tracking_issue="https://github.com/PolicyEngine/test/issues/4", + ) ) assert errors_of(record) == [] @@ -636,7 +731,12 @@ def test_a_skipped_producer_fails(): def test_an_unlisted_producer_fails(): errors = mutate( lambda r: _search(r).append( - {"producer": "made_up", "result": "none_found", "queries": ["q"], "claims": []} + { + "producer": "made_up", + "result": "none_found", + "queries": ["q"], + "claims": [], + } ) ) assert any("not on the UK list" in e for e in errors) @@ -685,7 +785,9 @@ def test_a_source_names_a_real_producer(source): def test_an_unused_primary_fails(): def f(r): - extra = dict(r.manifest[0], sha256="d" * 64, url="https://example.org/other.pdf") + extra = dict( + r.manifest[0], sha256="d" * 64, url="https://example.org/other.pdf" + ) r.manifest.append(extra) assert any("cited by no claim" in e for e in mutate(f)) @@ -714,13 +816,22 @@ def test_an_unhashed_primary_fails(): def test_a_publication_date_is_real_and_not_after_its_finding(): - assert any("real YYYY-MM-DD" in e for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-02-31"))) - assert any("after it was found" in e for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-10-30"))) + assert any( + "real YYYY-MM-DD" in e + for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-02-31")) + ) + assert any( + "after it was found" in e + for e in mutate(lambda r: claim0(r)["publication"].update(date="2026-10-30")) + ) def test_a_different_world_needs_a_reason(): def f(r): - r.analysis["worlds"]["other"] = {"policy": "uk_other", "description": "another world here"} + r.analysis["worlds"]["other"] = { + "policy": "uk_other", + "description": "another world here", + } claim0(r)["world"] = "other" assert any("compare across worlds only with" in e for e in mutate(f)) @@ -728,13 +839,23 @@ def f(r): def test_a_cross_world_comparison_is_not_plainly_comparable(): def f(r): - r.analysis["worlds"]["other"] = {"policy": "uk_other", "description": "another world here"} + r.analysis["worlds"]["other"] = { + "policy": "uk_other", + "description": "another world here", + } claim0(r)["world"] = "other" claim0(r)["compare_to"]["across_worlds"] = { "reason": "the quantities are the same in both worlds, as measured", - "basis": [{"kind": "issue", "ref": "https://github.com/PolicyEngine/test/issues/2"}], + "basis": [ + { + "kind": "issue", + "ref": "https://github.com/PolicyEngine/test/issues/2", + } + ], } - r.annotations.append(note("x", ["ext_cost_fy2027"], "comparable", explains=True)) + r.annotations.append( + note("x", ["ext_cost_fy2027"], "comparable", explains=True) + ) assert any("constructed, not comparable" in e for e in mutate(f)) @@ -745,12 +866,16 @@ def test_baselines_must_agree(): def test_a_world_baseline_is_registered(): - errors = mutate(lambda r: r.analysis["worlds"]["test_reform"].update(baseline="not_a_world")) + errors = mutate( + lambda r: r.analysis["worlds"]["test_reform"].update(baseline="not_a_world") + ) assert any("is not registered" in e for e in errors) def test_an_unregistered_claim_baseline_fails(): - errors = mutate(lambda r: claim0(r).update(baseline_policy="not_a_registered_world")) + errors = mutate( + lambda r: claim0(r).update(baseline_policy="not_a_registered_world") + ) assert any("is not registered" in e for e in errors) @@ -760,7 +885,11 @@ def test_a_restated_figure_is_not_compared(): def test_an_administrative_level_belongs_to_a_producer_source(): - errors = mutate(lambda r: claim0(r).update(benchmark_class="administrative_fact", world="baseline")) + errors = mutate( + lambda r: claim0(r).update( + benchmark_class="administrative_fact", world="baseline" + ) + ) assert any("calibration material" in e for e in errors) @@ -795,7 +924,9 @@ def name_it(r): def annotate(r): name_it(r) - r.annotations.append(note("net-vs-static", ["ext_cost_fy2027"], "concept_mismatch")) + r.annotations.append( + note("net-vs-static", ["ext_cost_fy2027"], "concept_mismatch") + ) assert mutate(annotate) == [] @@ -805,7 +936,9 @@ def lone(r): claim0(r).update(value_kind="range_low") claim0(r)["conditions"]["range_group"] = "g" - assert any("needs exactly one range_low and one range_high" in e for e in mutate(lone)) + assert any( + "needs exactly one range_low and one range_high" in e for e in mutate(lone) + ) def pair(pe, low, high): record = synthetic(pe, low) @@ -814,7 +947,13 @@ def pair(pe, low, high): c["conditions"]["range_group"] = "g" other = copy.deepcopy(c) printed = ("-" if high < 0 else "") + repr(abs(high)) - other.update(key="ext_cost_high_fy2027", value_kind="range_high", value=high, value_raw=printed, quote=f"Cost {printed}") + other.update( + key="ext_cost_high_fy2027", + value_kind="range_high", + value=high, + value_raw=printed, + quote=f"Cost {printed}", + ) record.claims.append(other) for p in _search(record): if p["producer"] == "ifs": @@ -833,24 +972,39 @@ def pair(pe, low, high): def test_an_issue_basis_is_a_github_issue(): def f(r): r.annotations.append( - {**note("x", ["ext_cost_fy2027"], "comparable", explains=True), "basis": [{"kind": "issue", "ref": "https://example.org/ticket/1"}]} + { + **note("x", ["ext_cost_fy2027"], "comparable", explains=True), + "basis": [{"kind": "issue", "ref": "https://example.org/ticket/1"}], + } ) assert any("GitHub issue or pull URL" in e for e in mutate(f)) def test_a_basis_may_be_a_file_in_this_repository(): - assert cmp._check_basis({"kind": "document", "ref": "docs/COMPARABLES.md#the-gap"}, "t") == [] + assert ( + cmp._check_basis( + {"kind": "document", "ref": "docs/COMPARABLES.md#the-gap"}, "t" + ) + == [] + ) assert cmp._check_basis({"kind": "document", "ref": "docs"}, "t") != [] assert cmp._check_basis({"kind": "document", "ref": "../../etc/passwd"}, "t") != [] def test_a_staged_claim_is_never_re_signed(): - assert cmp.check_value({"value": 1, "value_raw": "1", "normalization": "as printed", "sign": -1}, "t") + assert cmp.check_value( + {"value": 1, "value_raw": "1", "normalization": "as printed", "sign": -1}, "t" + ) def test_an_estimate_may_be_re_signed_with_a_reason(): - row = {"value": -434e6, "value_raw": "0.434", "normalization": "GBP bn x 1e9 -> GBP", "sign": -1} + row = { + "value": -434e6, + "value_raw": "0.434", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + } assert any("sign_basis" in e for e in cmp.check_value(row, "t", signed=True)) row["sign_basis"] = "printed as a positive cost" assert cmp.check_value(row, "t", signed=True) == [] @@ -870,7 +1024,9 @@ def test_duplicate_json_keys_fail(tmp_path): # --- the gap rule ---------------------------------------------------------------------- -nonzero = st.floats(min_value=-1e12, max_value=1e12, allow_nan=False).filter(lambda x: abs(x) > 1e-3) +nonzero = st.floats(min_value=-1e12, max_value=1e12, allow_nan=False).filter( + lambda x: abs(x) > 1e-3 +) @given(x=nonzero) @@ -887,12 +1043,16 @@ def test_gap_is_invariant_to_rescaling_both_values(pe, ext, k): @given(pe=nonzero, ext=nonzero) def test_opposite_convention_is_the_value_negated(pe, ext): - assert cmp.relative_gap(pe, -ext, signs.OPPOSITE) == cmp.relative_gap(pe, ext, signs.SAME) + assert cmp.relative_gap(pe, -ext, signs.OPPOSITE) == cmp.relative_gap( + pe, ext, signs.SAME + ) @given(pe=nonzero, ext=nonzero) def test_magnitudes_ignore_sign(pe, ext): - assert cmp.relative_gap(pe, ext, signs.MAGNITUDE) == cmp.relative_gap(abs(pe), abs(ext), signs.SAME) + assert cmp.relative_gap(pe, ext, signs.MAGNITUDE) == cmp.relative_gap( + abs(pe), abs(ext), signs.SAME + ) @given(pe=nonzero, ext=nonzero, t=st.floats(0.001, 0.999)) @@ -930,7 +1090,11 @@ def test_no_rule_no_gap(): ], ) def test_ambiguous_conventions_do_not_orient(convention): - claim = {"metric": "revenue_change", "conditions": {"sign_convention": convention}, "source": "x"} + claim = { + "metric": "revenue_change", + "conditions": {"sign_convention": convention}, + "source": "x", + } assert cmp.orientation(claim, "UK")[0] == signs.UNKNOWN @@ -942,9 +1106,10 @@ def test_the_policy_keeps_the_threshold_below_one(tmp_path): cmp.load_policy(path) -def test_the_policy_rejects_a_bad_window(tmp_path): +@pytest.mark.parametrize("days", [0, 91, 7.5, True, "7"]) +def test_the_policy_rejects_a_bad_window(tmp_path, days): path = tmp_path / "policy.json" - path.write_text(json.dumps(dict(POLICY, promotion_window_days=0))) + path.write_text(json.dumps(dict(POLICY, promotion_window_days=days))) with pytest.raises(ValueError, match="promotion_window_days"): cmp.load_policy(path) @@ -998,11 +1163,17 @@ def test_printed_forms(raw, value): @pytest.mark.parametrize( "raw, normalization", - [("£800m", "GBP bn x 1e9 -> GBP"), ("2.1%", "GBP bn x 1e9 -> GBP"), ("£0.8bn", "as printed")], + [ + ("£800m", "GBP bn x 1e9 -> GBP"), + ("2.1%", "GBP bn x 1e9 -> GBP"), + ("£0.8bn", "as printed"), + ], ) def test_a_printed_scale_must_match_the_normalization(raw, normalization): value = cmp.parse_printed(raw) * cmp.NORMALIZATIONS[normalization][0] - errors = cmp.check_value({"value": value, "value_raw": raw, "normalization": normalization}, "t") + errors = cmp.check_value( + {"value": value, "value_raw": raw, "normalization": normalization}, "t" + ) assert any("states the scale" in e for e in errors) @@ -1019,7 +1190,11 @@ def test_app_record_and_index_cite_the_same_search(directory): record = cmp.load_analysis(directory) block = cmp.app_record(record) entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] - search = next(s for s in entry["searches"] if s["date"] == block["searchDate"] and s["kind"] == block["searchKind"]) + search = next( + s + for s in entry["searches"] + if s["date"] == block["searchDate"] and s["kind"] == block["searchKind"] + ) assert block["scorecardSource"] == directory.name assert block["producersSearched"] == search["producers_searched"] assert block["found"] == search["found"] @@ -1031,7 +1206,9 @@ def test_index_comparisons_are_every_staged_estimate(directory): record = cmp.load_analysis(directory) entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][directory.name] rows = {r["key"]: r for r in cmp.evaluate(record, POLICY)} - assert [c["claim"] for c in entry["comparisons"]] == [c["key"] for c in record.claims] + assert [c["claim"] for c in entry["comparisons"]] == [ + c["key"] for c in record.claims + ] assert entry["current_pins"] == cmp.current_pins(record.analysis) for c in entry["comparisons"]: row = rows[c["claim"]] @@ -1049,7 +1226,9 @@ def test_scaffold_lists_every_producer(tmp_path): directory = cmp.scaffold("uk", "app", "new-dashboard", SEARCH_DATE, root=tmp_path) log = json.loads((directory / "search_log.json").read_text()) logged = [p["producer"] for p in log["searches"][0]["producers"]] - assert logged == [p["slug"] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE)] + assert logged == [ + p["slug"] for p in cmp.required_producers(PRODUCERS, "UK", SEARCH_DATE) + ] assert directory.name == "analysis-uk-new-dashboard" @@ -1072,15 +1251,47 @@ def test_no_estimate_is_only_for_posts_that_are_not_analyses(): def test_due_lists_recent_estimates_only(tmp_path): apps = [ - {"slug": "a", "countryId": "uk", "date": "2026-10-12 12:00:00", "displayWithResearch": True, "tags": ["uk", "policy"]}, - {"slug": "old", "countryId": "uk", "date": "2026-01-01", "displayWithResearch": True, "tags": ["uk", "policy"]}, - {"slug": "tool", "countryId": "us", "date": "2026-10-01", "displayWithResearch": True, "tags": ["us"]}, - {"slug": "unlisted", "countryId": "us", "date": "2026-10-03", "tags": ["us", "policy"]}, + { + "slug": "a", + "countryId": "uk", + "date": "2026-10-12 12:00:00", + "displayWithResearch": True, + "tags": ["uk", "policy"], + }, + { + "slug": "old", + "countryId": "uk", + "date": "2026-01-01", + "displayWithResearch": True, + "tags": ["uk", "policy"], + }, + { + "slug": "tool", + "countryId": "us", + "date": "2026-10-01", + "displayWithResearch": True, + "tags": ["us"], + }, + { + "slug": "unlisted", + "countryId": "us", + "date": "2026-10-03", + "tags": ["us", "policy"], + }, ] posts = [ {"filename": "Some_Post.md", "date": "2026-10-02", "tags": ["us", "policy"]}, - {"filename": "budget-costing.md", "date": "2026-10-04", "tags": ["uk", "autumn-budget"]}, - {"filename": "event.md", "date": "2026-10-03", "tags": ["uk", "event"], "comparables": {"status": "no_estimate", "reason": "x"}}, + { + "filename": "budget-costing.md", + "date": "2026-10-04", + "tags": ["uk", "autumn-budget"], + }, + { + "filename": "event.md", + "date": "2026-10-03", + "tags": ["uk", "event"], + "comparables": {"status": "no_estimate", "reason": "x"}, + }, {"filename": "untagged.md", "date": "2026-10-05", "tags": ["technical"]}, ] rows = cmp.due(apps, posts, "2026-10-21", 183, root=tmp_path) @@ -1094,7 +1305,15 @@ def test_due_lists_recent_estimates_only(tmp_path): def test_due_refuses_two_entries_sharing_a_record(tmp_path): - apps = [{"slug": "x", "countryId": "uk", "date": "2026-10-12", "displayWithResearch": True, "tags": ["uk", "policy"]}] + apps = [ + { + "slug": "x", + "countryId": "uk", + "date": "2026-10-12", + "displayWithResearch": True, + "tags": ["uk", "policy"], + } + ] posts = [{"filename": "x.md", "date": "2026-10-13", "tags": ["uk", "policy"]}] with pytest.raises(ValueError, match="would share the record analysis-uk-x"): cmp.due(apps, posts, "2026-10-21", 183, root=tmp_path) @@ -1107,7 +1326,11 @@ def _entry(search_dates, pending=(), pins_match=True): return { "current_pins": [PIN], "searches": [ - {"date": d, "kind": "reharvest", "results_pins": [PIN] if pins_match else ["x"]} + { + "date": d, + "kind": "reharvest", + "results_pins": [PIN] if pins_match else ["x"], + } for d in search_dates ], "comparisons": [ @@ -1128,7 +1351,11 @@ def test_fresh_exactly_when_a_search_falls_in_the_window(offsets, window): assert (cmp.fresh(_entry(dates), PUSH, window) == []) == in_window -@given(offsets=st.lists(days_st, min_size=1, max_size=6), w1=st.integers(1, 60), w2=st.integers(1, 60)) +@given( + offsets=st.lists(days_st, min_size=1, max_size=6), + w1=st.integers(1, 60), + w2=st.integers(1, 60), +) def test_a_wider_window_is_never_stricter(offsets, w1, w2): dates = sorted(shift(PUSH, o) for o in offsets) narrow, wide = sorted((w1, w2)) @@ -1219,7 +1446,11 @@ def test_db_holds_exactly_the_records_claims(db): claims = {c["key"]: c for c in record.claims} for row in cmp.evaluate(record, POLICY): c = claims[row["key"]] - if c["attribution"] == "restated" or c.get("metric") is None or c.get("unit_concept") is None: + if ( + c["attribution"] == "restated" + or c.get("metric") is None + or c.get("unit_concept") is None + ): continue expected[(record.analysis["id"], row["key"])] = (row, c) seen = set() @@ -1229,11 +1460,14 @@ def test_db_holds_exactly_the_records_claims(db): seen.add(key) row, claim = expected[key] results = db.execute( - "SELECT status, computed_value FROM pe_results WHERE claim_id = ?", (r["claim_id"],) + "SELECT status, computed_value FROM pe_results WHERE claim_id = ?", + (r["claim_id"],), ).fetchall() history = 1 + sum( 1 - for e in cmp.load_analysis(cmp.SOURCES / f"analysis-{key[0].replace('/', '-')}").analysis.get("superseded_estimates", []) + for e in cmp.load_analysis( + cmp.SOURCES / f"analysis-{key[0].replace('/', '-')}" + ).analysis.get("superseded_estimates", []) if e["key"] in (row["estimate"], row["nearest"]) ) if row["estimate"] or row["nearest"]: diff --git a/tests/test_export_populations.py b/tests/test_export_populations.py index a60f5222d..1ee5f2616 100644 --- a/tests/test_export_populations.py +++ b/tests/test_export_populations.py @@ -410,7 +410,7 @@ def test_integration_committed_db(tmp_path): # Pre-publish comparables (docs/COMPARABLES.md) add every staged claim of # each analysis record: 20 for the £100,000 childcare dashboard (12 CenTax, # 8 Centre for British Progress), with or without a result. - assert dist == {"US": 535, "UK": 5163 + 23_752 + 246 + 20, "BE": 9, "NZ": 12} + assert dist == {"US": 535, "UK": 5163 + 23_752 + 246 + 22, "BE": 9, "NZ": 12} nz = [row for row in payload["rows"] if row["country"] == "NZ"] assert len(nz) == 12 assert {row["source"] for row in nz} == {"nz_treasury"} @@ -445,8 +445,8 @@ def test_integration_committed_db(tmp_path): assert committed["summary"] == payload["summary"] assert committed["event_archives"] == payload["event_archives"] # Every event is reachable in the deployed copy, with bounded requests. - # 5,719 + the 20 pre-publish comparables rows of the childcare record - assert len(payload["rows"]) == 5719 + 20 + # 5,719 + the 22 pre-publish comparables rows of the childcare record + assert len(payload["rows"]) == 5719 + 22 for archive in payload["event_archives"]: relative = archive["path"].removeprefix("data/") freshly_exported = (tmp_path / relative).read_bytes() From 14d708fa8d3477d54a5256f7a0770726ff217700 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:09:15 -0400 Subject: [PATCH 09/15] Refuse a sign convention that names the other side of the ledger signs.py reads 'positive = increase in ...' as the same direction for a spending or tax metric whatever follows, so 'positive = increase in federal revenue' oriented a benefit cost as if it were spending. Such a convention is now unknown, and the claim cannot be compared until it is stated plainly. Co-Authored-By: Claude Opus 5.5 --- scorecard_db/comparables.py | 10 ++++++++++ tests/test_comparables.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 7826090b5..9ec06166e 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -333,6 +333,14 @@ def check_value(row: dict, where: str, signed: bool = False) -> list[str]: # Words that turn a "positive = ..." phrase around, which signs.py's substring # match would otherwise read as the plain phrase ("positive = yield forgone"). _INVERTING_WORDS = ("forgone", "foregone", "lost") +# signs.py reads "positive = increase in ..." as the same direction for these +# families whatever follows; a convention naming the other side of the +# ledger ("positive = increase in federal revenue" for a spending metric) +# is not that direction. +_OTHER_SIDE_WORDS = { + "spending": ("revenue", "receipt", "yield", "tax"), + "tax_increase": ("credit", "refund", "benefit"), +} def orientation(claim: dict, country: str) -> tuple[str, str]: @@ -358,6 +366,8 @@ def orientation(claim: dict, country: str) -> tuple[str, str]: return signs.UNKNOWN, "convention matches both directions" if hits_same and any(w in text for w in _INVERTING_WORDS): return signs.UNKNOWN, "convention turns its phrase around" + if hits_same and any(w in text for w in _OTHER_SIDE_WORDS.get(family, ())): + return signs.UNKNOWN, "convention names the other side of the ledger" return signs.classify( { "metric": metric, diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 61132c0f2..4e74c4525 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -1098,6 +1098,36 @@ def test_ambiguous_conventions_do_not_orient(convention): assert cmp.orientation(claim, "UK")[0] == signs.UNKNOWN +@pytest.mark.parametrize( + "metric, convention, relation", + [ + ( + "benefit_cost_change", + "positive = increase in federal revenue", + signs.UNKNOWN, + ), + ("benefit_cost_change", "positive = increase in tax receipts", signs.UNKNOWN), + ("benefit_cost_change", "positive = higher government expenditure", signs.SAME), + ("revenue_change", "positive = higher government revenue", signs.SAME), + ("revenue_change", "positive = increase in borrowing", signs.OPPOSITE), + ("avg_tax_change_usd", "positive = increase in tax credits", signs.UNKNOWN), + ("avg_tax_change_usd", "positive = tax increase", signs.SAME), + ], +) +def test_conventions_orient_only_when_they_say_one_thing(metric, convention, relation): + claim = { + "metric": metric, + "conditions": {"sign_convention": convention}, + "source": "x", + } + assert cmp.orientation(claim, "US")[0] == relation + + +def test_a_us_change_with_no_convention_does_not_orient(): + claim = {"metric": "revenue_change", "conditions": {}, "source": "cbo"} + assert cmp.orientation(claim, "US")[0] == signs.UNKNOWN + + def test_the_policy_keeps_the_threshold_below_one(tmp_path): for bad in (1, 1.5, 0, True): path = tmp_path / "policy.json" From 2337fa663ca1e171a495f475a78d18e8dba3dd6e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:11:40 -0400 Subject: [PATCH 10/15] Let a household calculator record its search with no fixed estimate A policy-tagged calculator must still be searched, but it publishes no aggregate to pin. It declares calculator with a reason, pins no estimates, and its searches compare no results file. Co-Authored-By: Claude Opus 5.5 --- docs/COMPARABLES.md | 5 +++++ scorecard_db/comparables.py | 30 +++++++++++++++++++++++++----- tests/test_comparables.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index c0dab9b72..e800b99f6 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -97,6 +97,11 @@ results file prints it the other way round (a cost as a positive number), the es An estimate that a rerun replaced moves to `superseded_estimates` with `superseded_on`, and stays on the scorecard as history. +A household calculator, which computes figures for the household a reader describes and +publishes no fixed estimate, is still searched: it declares `"calculator": {"reason": "..."}` +and may pin no estimates, its searches record `results_pins: []`, and every claim found says +in `pe_missing` that the calculator publishes no aggregate. + ### The search `data/comparables/producers.json` lists the producers for each country. A search logs every diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 9ec06166e..016f7e314 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -854,10 +854,22 @@ def validate( continue estimates[key] = e errors.extend(_check_estimate(e, f"{name}: estimate {key}", world_keys)) - if not estimates: + # A household calculator computes figures for the household a reader + # describes and publishes no fixed estimate: its producers are still + # searched (decision d1816's default), but it may pin none. + calculator = a.get("calculator") + if calculator is not None and not ( + isinstance(calculator, dict) + and set(calculator) == {"reason"} + and _nonempty_str(calculator.get("reason"), 20) + ): + errors.append( + f'{name}: calculator must be {{"reason": ""}}' + ) + if not estimates and calculator is None: errors.append( - f"{name}: pe_estimates is empty; an analysis that publishes no estimate " - "is declared no_estimate in the registry, not recorded here" + f"{name}: pe_estimates is empty; pin every figure the analysis publishes, " + "or declare a household calculator that publishes none (`calculator`)" ) superseded_pins = set() for i, e in enumerate(a.get("superseded_estimates") or []): @@ -956,9 +968,17 @@ def validate( f"{where}: a {skind} search is dated after the first search ({first_date})" ) if not _nonempty_str(s.get("by")): - errors.append(f"{where}: by must name the session or Subfleet job") + errors.append(f"{where}: by must name the session or job that ran it") pins = s.get("results_pins") - if not (_string_list(pins) and pins and all(_PIN.match(p) for p in pins)): + no_results = calculator is not None and not estimates + if no_results and pins != []: + errors.append( + f"{where}: a calculator with no estimates compares no results file " + "(results_pins: [])" + ) + elif not no_results and not ( + _string_list(pins) and pins and all(_PIN.match(p) for p in pins) + ): errors.append( f"{where}: results_pins must list the results files compared " "(repo@commit:sha256; `comparables pins` prints them)" diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 4e74c4525..323137feb 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -723,6 +723,35 @@ def _search(record): return record.search_log["searches"][0]["producers"] +def _calculator(record): + """The synthetic record as a household calculator: no fixed estimate.""" + record.analysis["pe_estimates"] = [] + record.analysis["calculator"] = { + "reason": "Computes the change for the household the reader describes." + } + record.search_log["searches"][0]["results_pins"] = [] + claim0(record).update( + compare_to=None, pe_missing="the calculator publishes no aggregate" + ) + return record + + +def test_a_household_calculator_is_searched_with_no_estimates(): + assert errors_of(_calculator(synthetic(-100.0, -105.0))) == [] + + +def test_only_a_declared_calculator_may_pin_no_estimates(): + record = _calculator(synthetic(-100.0, -105.0)) + record.analysis.pop("calculator") + assert any("pe_estimates is empty" in e for e in errors_of(record)) + record = _calculator(synthetic(-100.0, -105.0)) + record.analysis["calculator"] = {"reason": "short"} + assert any("calculator must be" in e for e in errors_of(record)) + record = _calculator(synthetic(-100.0, -105.0)) + record.search_log["searches"][0]["results_pins"] = [PIN] + assert any("compares no results file" in e for e in errors_of(record)) + + def test_a_skipped_producer_fails(): errors = mutate(lambda r: _search(r).pop(0)) assert any("producers not searched: ['obr']" in e for e in errors) From 5b7e98b4a8a86ae1c21614e082f0511f576f66a4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:27:26 -0400 Subject: [PATCH 11/15] Read a dash between two numbers as a range, and let a claim's basis be unstated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backfill records quote ranges such as ~£3-3.4bn, whose upper bound the quote check read as negative. A producer that does not say whether its figure allows for behaviour now stages basis unstated, with any account in basis_note; such a claim is never compared as the same quantity. Co-Authored-By: Claude Opus 5.5 --- docs/COMPARABLES.md | 5 ++++- scorecard_db/comparables.py | 35 ++++++++++++++++++++++++++--------- tests/test_comparables.py | 18 ++++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index e800b99f6..653535911 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -147,7 +147,10 @@ mechanically where they can be: `conditions.sign_convention`, in words `scorecard_db/signs.py` reads. A compared change metric needs a convention that starts `positive =` and says one thing. - **The same identity as an estimate**: `metric`, `unit_concept`, `period`, `time_basis`, - `conditions.basis` and `conditions.program`, with the same `conditions.fy` rule. + `conditions.basis` and `conditions.program`, with the same `conditions.fy` rule. The basis is + one word; a fuller account of it goes in `conditions.basis_note`. A claim whose producer does + not say whether it allows for behaviour has basis `unstated`: it is never the same quantity as + an estimate, so it can only sit beside one with a stated mismatch. - **Registered vocabulary** for `metric` and `unit_concept`, or `null` with a proposal (such a claim is staged but waits for its vocabulary decision before it reaches the database). - **Named baselines**: `baseline_policy` is a label in `scorecard_db/baselines.py`, or null for diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 016f7e314..6f15b84b3 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -104,6 +104,10 @@ # The basis of an estimate or claim: what it holds fixed. Required on every # estimate and claim, and part of what makes two figures the same quantity. BASES = ("static", "post_behavioural", "forecast", "outturn") +# A producer that does not say whether its figure allows for behaviour: the +# claim is staged, but it is never the same quantity as an estimate, so it +# can only sit beside one with a stated mismatch. +CLAIM_BASES = BASES + ("unstated",) # A claim compared with an estimate has one of these statuses; a mismatch # means it is not compared, so it never labels a compared claim. COMPARED_STATUSES = ( @@ -264,16 +268,28 @@ def printed_scale(value_raw: str) -> str | None: return None +_RANGE_DASH_BEFORE = re.compile( + r"\d\s*(?:bn|billion|mn|m|million|k|thousand|tn|trillion|%)?\s*" + r"[" + _MINUS + r"]\s*[£$€]?\s*$", + re.I, +) + + def quote_contains(quote: str, value_raw: str) -> bool: """Whether the quote prints this value as a whole number, sign included: - "0.07" is not in "10.07", and "800m" is not in "-800m".""" + "0.07" is not in "10.07", and "800m" is not in "-800m". A dash between two + numbers is a range, not a sign: "3.4bn" is in "£3-3.4bn".""" q = " ".join(str(quote).split()) raw = " ".join(str(value_raw).split()) if not raw: return False - lead = "" if raw[0] in _MINUS else _MINUS - pattern = rf"(? list[str]: @@ -677,15 +693,16 @@ def _check_bases(bases, where: str) -> list[str]: return errors -def _check_identity(row: dict, where: str) -> list[str]: +def _check_identity(row: dict, where: str, bases: tuple = BASES) -> list[str]: """The conditions that, with metric, unit, period, time basis and world, - say what quantity a figure is.""" + say what quantity a figure is. A free-text account of the basis goes in + conditions.basis_note.""" errors = [] conditions = row.get("conditions") if not _string_map(conditions): return [f"{where}: conditions must map strings to strings"] - if conditions.get("basis") not in BASES: - errors.append(f"{where}: conditions.basis must be one of {BASES}") + if conditions.get("basis") not in bases: + errors.append(f"{where}: conditions.basis must be one of {bases}") if not _SLUG.match(conditions.get("program", "")): errors.append(f"{where}: conditions.program must name the programme (a slug)") fy = conditions.get("fy") @@ -1152,7 +1169,7 @@ def validate( errors.append( f"{where}: time_basis {c.get('time_basis')!r} is not registered" ) - errors.extend(_check_identity(c, where)) + errors.extend(_check_identity(c, where, CLAIM_BASES)) world = c.get("world") if world not in world_keys: errors.append(f"{where}: world {world!r} is not defined in analysis.json") diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 323137feb..b48ca1d76 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -837,6 +837,11 @@ def test_a_quote_must_print_the_whole_number_with_its_sign(): assert not cmp.quote_contains("Cost -800m", "800m") assert cmp.quote_contains("Cost -800m and more", "-800m") assert cmp.quote_contains("Abolish Cliff 0.070 0.077 0.091", "0.077") + # a dash between two numbers is a range, not a sign + assert cmp.quote_contains("a net cost of ~£3-3.4bn compared", "3.4bn") + assert cmp.quote_contains("between £3bn – £3.4bn", "£3.4bn") + assert not cmp.quote_contains("a saving of -3.4bn", "3.4bn") + assert not cmp.quote_contains("costs £-3.4bn", "3.4bn") def test_an_unhashed_primary_fails(): @@ -1010,6 +1015,19 @@ def f(r): assert any("GitHub issue or pull URL" in e for e in mutate(f)) +def test_an_unstated_basis_is_never_the_same_quantity(): + def unstated(r): + claim0(r)["conditions"].update( + basis="unstated", basis_note="the producer does not say" + ) + + errors = mutate(unstated) + assert any("different quantity is not compared" in e for e in errors) + record = synthetic(-100.0, -105.0) + record.analysis["pe_estimates"][0]["conditions"]["basis"] = "unstated" + assert any("conditions.basis must be one of" in e for e in errors_of(record)) + + def test_a_basis_may_be_a_file_in_this_repository(): assert ( cmp._check_basis( From f9395eb9668905c2f5a270f63167356bc3239783 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:33:25 -0400 Subject: [PATCH 12/15] Stage a one-sided figure as a bound, set beside an estimate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Over £1 billion" is neither a point nor a range. lower_bound and upper_bound join cumulative as kinds that are never compared with a single-year estimate. Co-Authored-By: Claude Opus 5.5 --- docs/COMPARABLES.md | 8 ++++++-- scorecard_db/comparables.py | 13 ++++++++++--- tests/test_comparables.py | 1 + 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index 653535911..46f9a87b7 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -87,7 +87,9 @@ Each estimate in `pe_estimates` names its `world`, `metric`, `unit_concept`, `pe `time_basis`, and two conditions: - `conditions.basis`: `static`, `post_behavioural`, `forecast` or `outturn`; -- `conditions.program`: a slug for what is costed (`thirty_hours`, `both_schemes`). +- `conditions.program`: a slug for what is costed: the programmes or taxes whose change the + figure counts (`thirty_hours`, `both_schemes`, or `ny_dependent_exemption_ny_state_income_tax` + for a state tax expenditure beside a net budget effect). When `conditions.fy` names a fiscal year, `period` is its end year and `time_basis` is `fiscal_year`. Each estimate is stated in PolicyEngine's own orientation for its metric; when the @@ -161,7 +163,9 @@ mechanically where they can be: - **Primaries, hashed**: `publication.primary_sha256` is a line in `manifest.jsonl`, and `publication.date` is no later than the day the claim was found. - **Ranges**: a printed range is two claims, `value_kind` `range_low` and `range_high`, sharing a - `conditions.range_group`, the same identity and the same `compare_to`. + `conditions.range_group`, the same identity and the same `compare_to`. A one-sided figure + ("over £1 billion") is `lower_bound` or `upper_bound`, and like a `cumulative` total it is set + beside an estimate, never compared with one. ### The comparison diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 6f15b84b3..b7e232994 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -95,8 +95,14 @@ "range_high", "approx_chart_reading", "cumulative", + "lower_bound", + "upper_bound", ) RANGE_KINDS = ("range_low", "range_high") +# A figure that is not a single year's estimate of the quantity: a total over +# years, or a one-sided bound ("over £1 billion"). It is set beside an +# estimate, never compared with one. +UNCOMPARED_KINDS = ("cumulative", "lower_bound", "upper_bound") ATTRIBUTIONS = ("own", "restated", "same_assumptions") BENCHMARK_CLASSES = ("different_model", "administrative_fact", "same_assumptions") PARSE_CONFIDENCE = ("high", "medium", "low") @@ -1266,7 +1272,7 @@ def validate( if ( _same_quantity(c, est) and c.get("attribution") == "own" - and value_kind not in ("cumulative",) + and value_kind not in UNCOMPARED_KINDS and baseline == worlds.get(est.get("world"), {}).get("baseline") ): errors.append( @@ -1290,9 +1296,10 @@ def validate( errors.append( f"{where}: only a producer's own estimate is compared ({c.get('attribution')})" ) - if value_kind == "cumulative": + if value_kind in UNCOMPARED_KINDS: errors.append( - f"{where}: a cumulative figure is not compared with a single-year estimate" + f"{where}: a {value_kind} figure is not compared with a single-year " + "estimate; set it beside the nearest one" ) if c.get("world") != est.get("world"): across = compare.get("across_worlds") diff --git a/tests/test_comparables.py b/tests/test_comparables.py index b48ca1d76..cac8cd449 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -515,6 +515,7 @@ def _evade(fn): sign_convention="positive = reduction in borrowing; negative = increase in borrowing" ), "cumulative": lambda r: claim0(r).update(value_kind="cumulative"), + "a one-sided bound": lambda r: claim0(r).update(value_kind="lower_bound"), "relabelled concept mismatch": lambda r: r.annotations.append( note("x", ["ext_cost_fy2027"], "concept_mismatch", explains=True) ), From c4490d1df09943ec234b100ed5e940d241b799a8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:37:01 -0400 Subject: [PATCH 13/15] Make a money figure's price base part of its identity A figure in a base year's prices (DWP's real-terms table) is not the same quantity as a cash estimate. conditions.prices is cash when absent, real, or (for a claim) unstated. Co-Authored-By: Claude Opus 5.5 --- docs/COMPARABLES.md | 4 +++- scorecard_db/comparables.py | 28 +++++++++++++++++++--------- tests/test_comparables.py | 19 +++++++++++++++++++ 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index 46f9a87b7..29612f6f2 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -91,6 +91,7 @@ Each estimate in `pe_estimates` names its `world`, `metric`, `unit_concept`, `pe figure counts (`thirty_hours`, `both_schemes`, or `ny_dependent_exemption_ny_state_income_tax` for a state tax expenditure beside a net budget effect). +A money figure in a base year's prices has `conditions.prices: real`; absent means cash. When `conditions.fy` names a fiscal year, `period` is its end year and `time_basis` is `fiscal_year`. Each estimate is stated in PolicyEngine's own orientation for its metric; when the results file prints it the other way round (a cost as a positive number), the estimate carries @@ -149,7 +150,8 @@ mechanically where they can be: `conditions.sign_convention`, in words `scorecard_db/signs.py` reads. A compared change metric needs a convention that starts `positive =` and says one thing. - **The same identity as an estimate**: `metric`, `unit_concept`, `period`, `time_basis`, - `conditions.basis` and `conditions.program`, with the same `conditions.fy` rule. The basis is + `conditions.basis`, `conditions.program` and `conditions.prices` (`cash` when absent, `real`, + or `unstated` when the producer does not say), with the same `conditions.fy` rule. The basis is one word; a fuller account of it goes in `conditions.basis_note`. A claim whose producer does not say whether it allows for behaviour has basis `unstated`: it is never the same quantity as an estimate, so it can only sit beside one with a stated mismatch. diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index b7e232994..cc0c32b03 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -137,7 +137,17 @@ BANNED_SOURCES = ("open_search", "academic", "policyengine") # Everything that makes a claim and an estimate the same quantity. IDENTITY_FIELDS = ("metric", "unit_concept", "period", "time_basis", "world") -IDENTITY_CONDITIONS = ("basis", "program") +IDENTITY_CONDITIONS = ("basis", "program", "prices") +# conditions.prices: the price base of a money figure. Absent means cash +# (nominal); a figure in a base year's prices is "real", and a producer that +# does not say is "unstated", which is never the same quantity as an estimate. +PRICE_BASES = ("cash", "real", "unstated") + + +def _condition(row: dict, f: str): + value = (row.get("conditions") or {}).get(f) + return "cash" if f == "prices" and value is None else value + # What makes a policyengine.org entry publish an estimate, and when it may say # it publishes none. The same rule is in policyengine-app-v2's @@ -711,6 +721,10 @@ def _check_identity(row: dict, where: str, bases: tuple = BASES) -> list[str]: errors.append(f"{where}: conditions.basis must be one of {bases}") if not _SLUG.match(conditions.get("program", "")): errors.append(f"{where}: conditions.program must name the programme (a slug)") + prices = conditions.get("prices") + allowed_prices = PRICE_BASES if "unstated" in bases else ("cash", "real") + if prices is not None and prices not in allowed_prices: + errors.append(f"{where}: conditions.prices must be one of {allowed_prices}") fy = conditions.get("fy") if fy is not None: match = _FY.match(fy) @@ -770,9 +784,7 @@ def _check_estimate(e: dict, where: str, world_keys: set) -> list[str]: def _same_quantity(claim: dict, estimate: dict) -> bool: return all(claim.get(f) == estimate.get(f) for f in IDENTITY_FIELDS) and all( - (claim.get("conditions") or {}).get(f) - == (estimate.get("conditions") or {}).get(f) - for f in IDENTITY_CONDITIONS + _condition(claim, f) == _condition(estimate, f) for f in IDENTITY_CONDITIONS ) @@ -1322,8 +1334,8 @@ def validate( "a different quantity is not compared" ) for f in IDENTITY_CONDITIONS: - mine = (c.get("conditions") or {}).get(f) - theirs = (est.get("conditions") or {}).get(f) + mine = _condition(c, f) + theirs = _condition(est, f) if mine != theirs: errors.append( f"{where}: {f} {mine!r} differs from {est['key']}'s {theirs!r}; " @@ -1371,9 +1383,7 @@ def validate( "compare_to", ) if any(low.get(f) != high.get(f) for f in shared) or any( - (low.get("conditions") or {}).get(f) - != (high.get("conditions") or {}).get(f) - for f in IDENTITY_CONDITIONS + _condition(low, f) != _condition(high, f) for f in IDENTITY_CONDITIONS ): errors.append( f"{name}: range_group {group}'s bounds measure different things" diff --git a/tests/test_comparables.py b/tests/test_comparables.py index cac8cd449..4fdb35785 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -499,6 +499,7 @@ def _evade(fn): "basis relabelled to net": lambda r: claim0(r)["conditions"].update( basis="post_behavioural" ), + "price base relabelled": lambda r: claim0(r)["conditions"].update(prices="real"), "programme relabelled": lambda r: claim0(r)["conditions"].update( program="another_programme" ), @@ -1016,6 +1017,24 @@ def f(r): assert any("GitHub issue or pull URL" in e for e in mutate(f)) +def test_a_price_base_is_part_of_a_figures_identity(): + def real(r): + claim0(r)["conditions"]["prices"] = "real" + + assert any("prices 'real' differs" in e for e in mutate(real)) + # cash is the default on both sides + assert mutate(lambda r: claim0(r)["conditions"].update(prices="cash")) == [] + assert any( + "conditions.prices must be one of" in e + for e in mutate( + lambda r: claim0(r)["conditions"].update(prices="2025-26 prices") + ) + ) + record = synthetic(-100.0, -105.0) + record.analysis["pe_estimates"][0]["conditions"]["prices"] = "unstated" + assert any("conditions.prices must be one of" in e for e in errors_of(record)) + + def test_an_unstated_basis_is_never_the_same_quantity(): def unstated(r): claim0(r)["conditions"].update( From 3b7421f4659fb2b6c2e7c4fa9b5c2e711df8c702 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 12:38:17 -0400 Subject: [PATCH 14/15] Count a hashed primary as used when a basis or a checked page links it The backfill records hash the documents their annotations cite and the pages their searches checked; those lines are provenance, not orphans. Co-Authored-By: Claude Opus 5.5 --- docs/COMPARABLES.md | 2 +- scorecard_db/comparables.py | 16 ++++++++++++++-- tests/test_comparables.py | 19 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index 29612f6f2..1f61b560c 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -69,7 +69,7 @@ hyphens). |---|---| | `analysis.json` | The registry entry, scope, search terms, the policy worlds it scores, and every PolicyEngine estimate it publishes, each pinned to its results file by repo, commit, path, field and SHA-256 | | `search_log.json` | Every search: date, kind (`prepublish`, `backfill`, `reharvest`, `prepromotion`), who ran it, the results files it compared, and one entry per listed producer | -| `manifest.jsonl` | One line per primary publication, with its SHA-256; every line is cited by a claim | +| `manifest.jsonl` | One line per primary publication, with its SHA-256; every line is cited by a claim, an annotation basis or a page a search checked | | `claims_staged.jsonl` | Every comparable found, staged under the harvest rules | | `annotations.json` | The status and cause of anything that is not a plain comparison, each citing a basis | diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index cc0c32b03..8f739daaf 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -1360,10 +1360,22 @@ def validate( for key in claim_found: if key not in claims_by_key: errors.append(f"{name}: search lists claim {key}, which is not staged") + # A hashed primary is used when a claim cites it, an annotation's basis + # links it, or a search lists it among the pages it checked. + linked = set() + for n in record.annotations if isinstance(record.annotations, list) else []: + for basis in n.get("basis") or [] if isinstance(n, dict) else []: + if isinstance(basis, dict) and isinstance(basis.get("ref"), str): + linked.add(basis["ref"].split("#")[0]) + for s in searches if isinstance(searches, list) else []: + for p in (s.get("producers") or []) if isinstance(s, dict) else []: + if isinstance(p, dict) and _string_list(p.get("checked")): + linked.update(p["checked"]) for sha, m in manifest.items(): - if sha not in used_primaries: + if sha not in used_primaries and m.get("url") not in linked: errors.append( - f"{name}: manifest line for {m.get('url')} is cited by no claim" + f"{name}: manifest line for {m.get('url')} is cited by no claim, " + "annotation basis or checked page" ) for group, members in range_groups.items(): kinds = sorted(m.get("value_kind") for m in members) diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 4fdb35785..2f6ec1f28 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -823,6 +823,25 @@ def f(r): assert any("cited by no claim" in e for e in mutate(f)) + def checked(r): + f(r) + _search(r)[0]["checked"] = ["https://example.org/other.pdf"] + + assert mutate(checked) == [] + + def linked(r): + f(r) + r.annotations.append( + { + **note("x", ["ext_cost_fy2027"], "comparable"), + "basis": [ + {"kind": "document", "ref": "https://example.org/other.pdf#page=2"} + ], + } + ) + + assert not any("cited by no claim" in e for e in mutate(linked)) + def test_a_value_that_is_not_what_was_printed_fails(): errors = mutate(lambda r: claim0(r).update(value=-999.0)) From 0ebe6d8208d11f992d5b6841c16094f5aa78029e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 13:47:04 -0400 Subject: [PATCH 15/15] Cite the latest search in a registry block, and record where an analysis is served record now prints the latest search (or the one asked for by date or kind), so a revised analysis returning to the site can cite its new search. report runs while gaps are unexplained. analysis.json may list deployments, which the index publishes so a delisted dashboard cannot be promoted through its own URL; the childcare record lists its Vercel deployment. A bill scored on the state legislative tracker is recorded as registry kind tracker, and due lists it. Co-Authored-By: Claude Opus 5.5 --- data/comparables/index.json | 3 + docs/COMPARABLES.md | 12 + scorecard_db/comparables.py | 77 +- .../analysis.json | 2365 +++++++++-------- tests/test_comparables.py | 69 + 5 files changed, 1337 insertions(+), 1189 deletions(-) diff --git a/data/comparables/index.json b/data/comparables/index.json index 1599bc131..d14c0fcfe 100644 --- a/data/comparables/index.json +++ b/data/comparables/index.json @@ -53,6 +53,9 @@ "slug": "childcare-100k-threshold", "title": "Scrapping the childcare cliff edge", "url": "https://policyengine.org/uk/childcare-100k-threshold", + "deployments": [ + "https://uk-childcare-100k-threshold.vercel.app/uk/childcare-100k-threshold" + ], "owner": "vahid-ahmadi", "published": "2026-10-12", "current_pins": [ diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md index 1f61b560c..b84b9c4b1 100644 --- a/docs/COMPARABLES.md +++ b/docs/COMPARABLES.md @@ -81,6 +81,12 @@ PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk --kind app The JSON files are strict: a repeated key fails the record. +`analysis.json` may list `deployments`: the other https URLs the analysis is served from, such as +its own Vercel deployment. PolicyEngine/newsletters holds a link to any of them to the record, so +a dashboard taken off policyengine.org cannot be promoted through its deployment URL. A bill +scored on the state legislative tracker, which no registry lists, is recorded with +`registry.kind: tracker`; `due` lists such records by their `published` date. + ### The estimates Each estimate in `pe_estimates` names its `world`, `metric`, `unit_concept`, `period` and @@ -234,6 +240,9 @@ posts.json entry is printed by: PYTHONPATH=. python -m scorecard_db.comparables record analysis-uk- ``` +It cites the record's latest search; `--search-date` and `--search-kind` pick another. A revised +analysis returning to the site cites the new search its results were compared in. + ```json "comparables": { "status": "reviewed", @@ -259,3 +268,6 @@ PYTHONPATH=. python -m scorecard_db.comparables due --apps apps.json --posts pos # what a re-harvest found since a date, and its gaps over the threshold PYTHONPATH=. python -m scorecard_db.comparables report analysis-uk- --since 2026-10-26 ``` + +`report` runs on a record whose only errors are gaps not yet explained, listing those errors on +stderr; any other error refuses the record. diff --git a/scorecard_db/comparables.py b/scorecard_db/comparables.py index 8f739daaf..b998a0a13 100644 --- a/scorecard_db/comparables.py +++ b/scorecard_db/comparables.py @@ -99,6 +99,10 @@ "upper_bound", ) RANGE_KINDS = ("range_low", "range_high") +# Where the analysis is listed: an apps.json app, a posts.json post, or a bill +# scored on the state legislative tracker (/reform-pipeline's tracker route), +# which no registry lists. +REGISTRY_KINDS = ("app", "post", "tracker") # A figure that is not a single year's estimate of the quantity: a total over # years, or a one-sided bound ("over £1 billion"). It is set beside an # estimate, never compared with one. @@ -200,6 +204,9 @@ def _condition(row: dict, f: str): _URL = re.compile(r"^https?://\S+$") # A document or diagnostic URL names a page, not just a host. _URL_WITH_PATH = re.compile(r"^https://[^/\s]+\.[^/\s]+/\S+$") +# a page an analysis is served from: an https host with a dot, any path +_PAGE_URL = re.compile(r"^https://[a-z0-9-]+(?:\.[a-z0-9-]+)+(?::\d+)?(?:/\S*)?$") +UNEXPLAINED = "no annotation explains it" # Harvest rule 1: ``value`` is ``value_raw`` normalised to the unit, with the # conversion named, and the scale a printed value states must be the one the @@ -826,8 +833,8 @@ def validate( return errors registry = a.get("registry") or {} kind, slug = registry.get("kind"), registry.get("slug") - if kind not in ("app", "post"): - errors.append(f"{name}: registry.kind must be 'app' or 'post'") + if kind not in REGISTRY_KINDS: + errors.append(f"{name}: registry.kind must be one of {REGISTRY_KINDS}") if not (isinstance(slug, str) and _REGISTRY_SLUG.match(slug)): errors.append(f"{name}: registry.slug {slug!r} is not a policyengine.org slug") if registry.get("country_id") != country.lower(): @@ -839,6 +846,16 @@ def validate( errors.append(f"{name}: id must be {country.lower()}/{slug}") if not (isinstance(a.get("url"), str) and a["url"].startswith("https://")): errors.append(f"{name}: url must be the analysis's https URL") + deployments = a.get("deployments", []) + if not ( + _string_list(deployments) + and all(_PAGE_URL.match(u) for u in deployments) + and len(set(deployments)) == len(deployments) + ): + errors.append( + f"{name}: deployments lists each https URL the analysis is also served " + "from (its own deployment, a preview host), once" + ) if not _nonempty_str(a.get("title")): errors.append(f"{name}: title is empty") scope = a.get("scope") @@ -1493,7 +1510,7 @@ def validate( errors.append( f"{name}: claim {row['key']} is {shown} from {row['estimate']}" f"{' (its sign disagrees)' if row['sign_disagrees'] else ''} " - f"(threshold {policy['gap_threshold']:.0%}) and no annotation explains it" + f"(threshold {policy['gap_threshold']:.0%}) and {UNEXPLAINED}" ) elif note["status"] != "pending" and not note.get("explains_gap"): errors.append( @@ -1632,6 +1649,7 @@ def index_entry(record: AnalysisRecord, policy: dict) -> dict: "slug": a["registry"]["slug"], "title": a["title"], "url": a["url"], + "deployments": a.get("deployments", []), "owner": a["owner"]["author"], "published": a.get("published"), "current_pins": current_pins(a), @@ -1759,14 +1777,24 @@ def render_index(index: dict) -> str: # --- the app-v2 record ------------------------------------------------------------ -def app_record(record: AnalysisRecord, search_date: str | None = None) -> dict: - """The ``comparables`` block for the analysis's apps.json / posts.json entry.""" - searches = [s for s in record.search_log["searches"] if s["kind"] in FIRST_KINDS] +def app_record( + record: AnalysisRecord, + search_date: str | None = None, + search_kind: str | None = None, +) -> dict: + """The ``comparables`` block for the analysis's apps.json / posts.json entry: + the latest search, or the latest on ``search_date`` of ``search_kind``. A + listing cites the search made before it; a revised analysis returning to + the site cites the new search its results were compared in.""" + searches = list(record.search_log["searches"]) if search_date: searches = [s for s in searches if s["date"] == search_date] + if search_kind: + searches = [s for s in searches if s["kind"] == search_kind] if not searches: raise ValueError( - f"{record.name}: no prepublish or backfill search on {search_date}" + f"{record.name}: no {search_kind or ''} search" + f"{' on ' + search_date if search_date else ''}".replace(" ", " ") ) summary = _search_summary(searches[-1]) return { @@ -1959,6 +1987,30 @@ def due( "recorded": (root / name).is_dir(), } ) + # bills scored on the state legislative tracker are listed in no registry; + # their records say when they were published + for directory in analysis_dirs(root): + record = load_analysis(directory) + a = record.analysis + registry = a.get("registry") or {} + published = a.get("published") + if registry.get("kind") != "tracker" or not published: + continue + if not (start <= published <= today): + continue + rows.append( + { + "kind": "tracker", + "country_ids": [registry.get("country_id")], + "slug": registry.get("slug"), + "date": published, + "title": a.get("title", ""), + "authors": [(a.get("owner") or {}).get("author")], + "tags": [], + "source": directory.name, + "recorded": True, + } + ) return sorted(rows, key=lambda r: (r["date"], r["source"])) @@ -2057,6 +2109,7 @@ def _main(argv: list[str]) -> int: p_record = sub.add_parser("record", help="print the app-v2 comparables block") p_record.add_argument("source") p_record.add_argument("--search-date") + p_record.add_argument("--search-kind", choices=SEARCH_KINDS) p_due = sub.add_parser("due", help="analyses due for re-harvest") p_due.add_argument("--apps", required=True) p_due.add_argument("--posts", required=True) @@ -2107,13 +2160,21 @@ def _main(argv: list[str]) -> int: record = load_analysis(SOURCES / args.source) policy = load_policy() errors = validate(record, policy=policy) + # a report is what a search found, gaps it has not yet explained + # included; any other error means the record cannot be read + unexplained = [e for e in errors if UNEXPLAINED in e] + if args.cmd == "report" and errors and len(unexplained) == len(errors): + print("\n".join(unexplained), file=sys.stderr) + errors = [] if errors: print("\n".join(errors), file=sys.stderr) return 1 if args.cmd == "record": print( json.dumps( - app_record(record, args.search_date), indent=2, ensure_ascii=False + app_record(record, args.search_date, args.search_kind), + indent=2, + ensure_ascii=False, ) ) return 0 diff --git a/sources/analysis-uk-childcare-100k-threshold/analysis.json b/sources/analysis-uk-childcare-100k-threshold/analysis.json index 8ec4464de..3615eb1fd 100644 --- a/sources/analysis-uk-childcare-100k-threshold/analysis.json +++ b/sources/analysis-uk-childcare-100k-threshold/analysis.json @@ -1,1221 +1,1224 @@ { - "schema": "comparables/analysis/1", - "id": "uk/childcare-100k-threshold", - "country": "UK", - "registry": { - "kind": "app", - "slug": "childcare-100k-threshold", - "country_id": "uk" - }, - "title": "Scrapping the childcare cliff edge", - "url": "https://policyengine.org/uk/childcare-100k-threshold", - "published": "2026-10-12", - "owner": { - "author": "vahid-ahmadi" - }, - "scope": "uk_wide", - "subject": "The cost of removing the £100,000 adjusted-net-income limit on the 30 funded hours for working parents (England) and on Tax-Free Childcare (UK-wide), static and net of the labour-supply response, who gains, and the cliff it removes.", - "search_terms": [ - "£100,000 childcare cliff edge", - "100k childcare limit cost", - "30 funded hours income limit", - "tax-free childcare £100,000 limit", - "abolish childcare cliff edge cost" - ], - "worlds": { - "both_limits_removed": { - "policy": "uk_free_childcare_hours_and_tfc_100k_limit_removed", - "description": "The dashboard's reform: the £100,000 limit removed from both the 30 funded hours for working parents and Tax-Free Childcare." + "schema": "comparables/analysis/1", + "id": "uk/childcare-100k-threshold", + "country": "UK", + "registry": { + "kind": "app", + "slug": "childcare-100k-threshold", + "country_id": "uk" }, - "free_hours_limit_removed": { - "policy": "uk_free_childcare_hours_100k_limit_removed", - "description": "The £100,000 limit removed from the free hours for working parents only; Tax-Free Childcare keeps its limit (CenTax section 4.2.1)." - } - }, - "pe_estimates": [ - { - "key": "thirty_hours_static_fy2026", - "label": "Static cost of the 30 funded hours, both limits removed, 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -410000000, - "value_raw": "0.41", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.thirty_hours.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" - }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } + "title": "Scrapping the childcare cliff edge", + "url": "https://policyengine.org/uk/childcare-100k-threshold", + "deployments": [ + "https://uk-childcare-100k-threshold.vercel.app/uk/childcare-100k-threshold" + ], + "published": "2026-10-12", + "owner": { + "author": "vahid-ahmadi" }, - { - "key": "thirty_hours_static_fy2027", - "label": "Static cost of the 30 funded hours, both limits removed, 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -434000000, - "value_raw": "0.434", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.thirty_hours.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + "scope": "uk_wide", + "subject": "The cost of removing the £100,000 adjusted-net-income limit on the 30 funded hours for working parents (England) and on Tax-Free Childcare (UK-wide), static and net of the labour-supply response, who gains, and the cliff it removes.", + "search_terms": [ + "£100,000 childcare cliff edge", + "100k childcare limit cost", + "30 funded hours income limit", + "tax-free childcare £100,000 limit", + "abolish childcare cliff edge cost" + ], + "worlds": { + "both_limits_removed": { + "policy": "uk_free_childcare_hours_and_tfc_100k_limit_removed", + "description": "The dashboard's reform: the £100,000 limit removed from both the 30 funded hours for working parents and Tax-Free Childcare." }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } + "free_hours_limit_removed": { + "policy": "uk_free_childcare_hours_100k_limit_removed", + "description": "The £100,000 limit removed from the free hours for working parents only; Tax-Free Childcare keeps its limit (CenTax section 4.2.1)." + } }, - { - "key": "thirty_hours_static_fy2028", - "label": "Static cost of the 30 funded hours, both limits removed, 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -484000000, - "value_raw": "0.484", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.thirty_hours.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + "pe_estimates": [ + { + "key": "thirty_hours_static_fy2026", + "label": "Static cost of the 30 funded hours, both limits removed, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -410000000, + "value_raw": "0.41", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "thirty_hours_static_fy2029", - "label": "Static cost of the 30 funded hours, both limits removed, 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -505000000, - "value_raw": "0.505", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.thirty_hours.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "thirty_hours_static_fy2027", + "label": "Static cost of the 30 funded hours, both limits removed, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -434000000, + "value_raw": "0.434", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_fy2026", - "label": "Static cost of both schemes, both limits removed, 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -558000000, - "value_raw": "0.558", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.total.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "thirty_hours_static_fy2028", + "label": "Static cost of the 30 funded hours, both limits removed, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -484000000, + "value_raw": "0.484", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_fy2027", - "label": "Static cost of both schemes, both limits removed, 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -593000000, - "value_raw": "0.593", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.total.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "thirty_hours_static_fy2029", + "label": "Static cost of the 30 funded hours, both limits removed, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -505000000, + "value_raw": "0.505", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.thirty_hours.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_fy2028", - "label": "Static cost of both schemes, both limits removed, 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -652000000, - "value_raw": "0.652", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.total.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_fy2026", + "label": "Static cost of both schemes, both limits removed, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -558000000, + "value_raw": "0.558", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_fy2029", - "label": "Static cost of both schemes, both limits removed, 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -678000000, - "value_raw": "0.678", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.gross_bn.total.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_fy2027", + "label": "Static cost of both schemes, both limits removed, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -593000000, + "value_raw": "0.593", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_low_fy2026", - "label": "Low end of the static cost range, both schemes, 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -543000000, - "value_raw": "0.543", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.low.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_fy2028", + "label": "Static cost of both schemes, both limits removed, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -652000000, + "value_raw": "0.652", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_high_fy2026", - "label": "High end of the static cost range, both schemes, 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -1160000000, - "value_raw": "1.16", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.high.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_fy2029", + "label": "Static cost of both schemes, both limits removed, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -678000000, + "value_raw": "0.678", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.gross_bn.total.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_low_fy2027", - "label": "Low end of the static cost range, both schemes, 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -578000000, - "value_raw": "0.578", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.low.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_low_fy2026", + "label": "Low end of the static cost range, both schemes, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -543000000, + "value_raw": "0.543", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_high_fy2027", - "label": "High end of the static cost range, both schemes, 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -1240000000, - "value_raw": "1.24", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.high.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_high_fy2026", + "label": "High end of the static cost range, both schemes, 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -1160000000, + "value_raw": "1.16", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_low_fy2028", - "label": "Low end of the static cost range, both schemes, 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -635000000, - "value_raw": "0.635", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.low.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_low_fy2027", + "label": "Low end of the static cost range, both schemes, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -578000000, + "value_raw": "0.578", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_high_fy2028", - "label": "High end of the static cost range, both schemes, 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -1371000000, - "value_raw": "1.371", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.high.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_high_fy2027", + "label": "High end of the static cost range, both schemes, 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -1240000000, + "value_raw": "1.24", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_low_fy2029", - "label": "Low end of the static cost range, both schemes, 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -652000000, - "value_raw": "0.652", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.low.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_low_fy2028", + "label": "Low end of the static cost range, both schemes, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -635000000, + "value_raw": "0.635", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_static_range_high_fy2029", - "label": "High end of the static cost range, both schemes, 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -1433000000, - "value_raw": "1.433", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "scenario": "range_high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "budget.range_bn.high.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_high_fy2028", + "label": "High end of the static cost range, both schemes, 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -1371000000, + "value_raw": "1.371", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_central_fy2026", - "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -451000000, - "value_raw": "0.451", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "central" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.central.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_low_fy2029", + "label": "Low end of the static cost range, both schemes, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -652000000, + "value_raw": "0.652", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.low.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_central_fy2027", - "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -479000000, - "value_raw": "0.479", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "central" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.central.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_static_range_high_fy2029", + "label": "High end of the static cost range, both schemes, 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -1433000000, + "value_raw": "1.433", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "scenario": "range_high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "budget.range_bn.high.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_central_fy2028", - "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -533000000, - "value_raw": "0.533", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "central" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.central.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_central_fy2026", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -451000000, + "value_raw": "0.451", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_central_fy2029", - "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -556000000, - "value_raw": "0.556", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "central" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.central.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_central_fy2027", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -479000000, + "value_raw": "0.479", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_low_fy2026", - "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -523000000, - "value_raw": "0.523", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.low.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_central_fy2028", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -533000000, + "value_raw": "0.533", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_low_fy2027", - "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -555000000, - "value_raw": "0.555", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.low.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_central_fy2029", + "label": "Cost of both schemes net of the labour-supply response (central elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -556000000, + "value_raw": "0.556", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "central" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.central.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_low_fy2028", - "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -613000000, - "value_raw": "0.613", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.low.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_low_fy2026", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -523000000, + "value_raw": "0.523", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_low_fy2029", - "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -637000000, - "value_raw": "0.637", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "low" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.low.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_low_fy2027", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -555000000, + "value_raw": "0.555", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_high_fy2026", - "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2026-27", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2027, - "time_basis": "fiscal_year", - "value": -344000000, - "value_raw": "0.344", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2026-27", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.high.2026", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_low_fy2028", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -613000000, + "value_raw": "0.613", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_high_fy2027", - "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2027-28", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2028, - "time_basis": "fiscal_year", - "value": -365000000, - "value_raw": "0.365", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2027-28", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.high.2027", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_low_fy2029", + "label": "Cost of both schemes net of the labour-supply response (low elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -637000000, + "value_raw": "0.637", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "low" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.low.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_high_fy2028", - "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2028-29", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2029, - "time_basis": "fiscal_year", - "value": -415000000, - "value_raw": "0.415", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2028-29", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.high.2028", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_high_fy2026", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2026-27", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2027, + "time_basis": "fiscal_year", + "value": -344000000, + "value_raw": "0.344", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2026-27", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2026", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "both_schemes_dynamic_high_fy2029", - "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2029-30", - "world": "both_limits_removed", - "metric": "revenue_change", - "unit_concept": "gbp", - "period": 2030, - "time_basis": "fiscal_year", - "value": -432000000, - "value_raw": "0.432", - "normalization": "GBP bn x 1e9 -> GBP", - "sign": -1, - "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", - "conditions": { - "fy": "2029-30", - "basis": "post_behavioural", - "program": "free_childcare_hours_and_tax_free_childcare", - "sign_convention": "positive = yield to the Exchequer", - "elasticity": "high" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "labour_supply.dynamic_cost_bn.high.2029", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_high_fy2027", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2027-28", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2028, + "time_basis": "fiscal_year", + "value": -365000000, + "value_raw": "0.365", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2027-28", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2027", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_fy2026", - "label": "Families gaining, 2026-27", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2027, - "time_basis": "fiscal_year", - "value": 270000, - "value_raw": "270000", - "normalization": "as printed", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2026.families_gaining", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_high_fy2028", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2028-29", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2029, + "time_basis": "fiscal_year", + "value": -415000000, + "value_raw": "0.415", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2028-29", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2028", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_thirty_hours_fy2026", - "label": "Families gaining through the 30 funded hours, 2026-27", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2027, - "time_basis": "fiscal_year", - "value": 87000, - "value_raw": "87000", - "normalization": "as printed", - "conditions": { - "fy": "2026-27", - "basis": "static", - "program": "free_childcare_hours" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2026.by_scheme.thirty_hours", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "both_schemes_dynamic_high_fy2029", + "label": "Cost of both schemes net of the labour-supply response (high elasticities), 2029-30", + "world": "both_limits_removed", + "metric": "revenue_change", + "unit_concept": "gbp", + "period": 2030, + "time_basis": "fiscal_year", + "value": -432000000, + "value_raw": "0.432", + "normalization": "GBP bn x 1e9 -> GBP", + "sign": -1, + "sign_basis": "results.json prints each cost as a positive number of pounds billion; revenue_change is stated positive = yield to the Exchequer, so a cost is negative", + "conditions": { + "fy": "2029-30", + "basis": "post_behavioural", + "program": "free_childcare_hours_and_tax_free_childcare", + "sign_convention": "positive = yield to the Exchequer", + "elasticity": "high" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "labour_supply.dynamic_cost_bn.high.2029", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_fy2027", - "label": "Families gaining, 2027-28", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2028, - "time_basis": "fiscal_year", - "value": 278000, - "value_raw": "278000", - "normalization": "as printed", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2027.families_gaining", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_fy2026", + "label": "Families gaining, 2026-27", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2027, + "time_basis": "fiscal_year", + "value": 270000, + "value_raw": "270000", + "normalization": "as printed", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2026.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_thirty_hours_fy2027", - "label": "Families gaining through the 30 funded hours, 2027-28", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2028, - "time_basis": "fiscal_year", - "value": 90000, - "value_raw": "90000", - "normalization": "as printed", - "conditions": { - "fy": "2027-28", - "basis": "static", - "program": "free_childcare_hours" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2027.by_scheme.thirty_hours", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_thirty_hours_fy2026", + "label": "Families gaining through the 30 funded hours, 2026-27", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2027, + "time_basis": "fiscal_year", + "value": 87000, + "value_raw": "87000", + "normalization": "as printed", + "conditions": { + "fy": "2026-27", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2026.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_fy2028", - "label": "Families gaining, 2028-29", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2029, - "time_basis": "fiscal_year", - "value": 297000, - "value_raw": "297000", - "normalization": "as printed", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2028.families_gaining", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_fy2027", + "label": "Families gaining, 2027-28", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2028, + "time_basis": "fiscal_year", + "value": 278000, + "value_raw": "278000", + "normalization": "as printed", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2027.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_thirty_hours_fy2028", - "label": "Families gaining through the 30 funded hours, 2028-29", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2029, - "time_basis": "fiscal_year", - "value": 98000, - "value_raw": "98000", - "normalization": "as printed", - "conditions": { - "fy": "2028-29", - "basis": "static", - "program": "free_childcare_hours" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2028.by_scheme.thirty_hours", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_thirty_hours_fy2027", + "label": "Families gaining through the 30 funded hours, 2027-28", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2028, + "time_basis": "fiscal_year", + "value": 90000, + "value_raw": "90000", + "normalization": "as printed", + "conditions": { + "fy": "2027-28", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2027.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_fy2029", - "label": "Families gaining, 2029-30", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2030, - "time_basis": "fiscal_year", - "value": 304000, - "value_raw": "304000", - "normalization": "as printed", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours_and_tax_free_childcare" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2029.families_gaining", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_fy2028", + "label": "Families gaining, 2028-29", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2029, + "time_basis": "fiscal_year", + "value": 297000, + "value_raw": "297000", + "normalization": "as printed", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2028.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - }, - { - "key": "families_gaining_thirty_hours_fy2029", - "label": "Families gaining through the 30 funded hours, 2029-30", - "world": "both_limits_removed", - "metric": "gainer_count", - "unit_concept": "families", - "period": 2030, - "time_basis": "fiscal_year", - "value": 101000, - "value_raw": "101000", - "normalization": "as printed", - "conditions": { - "fy": "2029-30", - "basis": "static", - "program": "free_childcare_hours" - }, - "provenance": { - "repo": "PolicyEngine/uk-childcare-100k-threshold", - "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", - "path": "data/results.json", - "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", - "field": "recipients.2029.by_scheme.thirty_hours", - "computed_at": "2026-10-09T11:50:21+00:00", - "engine": { - "policyengine": "6.2.1", - "policyengine_uk": "2.102.3" + { + "key": "families_gaining_thirty_hours_fy2028", + "label": "Families gaining through the 30 funded hours, 2028-29", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2029, + "time_basis": "fiscal_year", + "value": 98000, + "value_raw": "98000", + "normalization": "as printed", + "conditions": { + "fy": "2028-29", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2028.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + }, + { + "key": "families_gaining_fy2029", + "label": "Families gaining, 2029-30", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2030, + "time_basis": "fiscal_year", + "value": 304000, + "value_raw": "304000", + "normalization": "as printed", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours_and_tax_free_childcare" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2029.families_gaining", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } }, - "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" - } - } - ] + { + "key": "families_gaining_thirty_hours_fy2029", + "label": "Families gaining through the 30 funded hours, 2029-30", + "world": "both_limits_removed", + "metric": "gainer_count", + "unit_concept": "families", + "period": 2030, + "time_basis": "fiscal_year", + "value": 101000, + "value_raw": "101000", + "normalization": "as printed", + "conditions": { + "fy": "2029-30", + "basis": "static", + "program": "free_childcare_hours" + }, + "provenance": { + "repo": "PolicyEngine/uk-childcare-100k-threshold", + "commit": "f25f0dbd46c31d336bde06898d801a1887a8cf89", + "path": "data/results.json", + "sha256": "4193f13b2a1f40a809d5738a96f6558a3633978bd3bd5efd4da5db211b72f50a", + "field": "recipients.2029.by_scheme.thirty_hours", + "computed_at": "2026-10-09T11:50:21+00:00", + "engine": { + "policyengine": "6.2.1", + "policyengine_uk": "2.102.3" + }, + "dataset": "microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68" + } + } + ] } diff --git a/tests/test_comparables.py b/tests/test_comparables.py index 2f6ec1f28..87583b242 100644 --- a/tests/test_comparables.py +++ b/tests/test_comparables.py @@ -1338,6 +1338,75 @@ def test_index_comparisons_are_every_staged_estimate(directory): assert c["cause"], f"{c['claim']} is over the threshold with no cause" +def test_record_cites_the_latest_search_or_the_one_asked_for(): + record = post_publication(synthetic(-100.0, -105.0), kind="prepromotion") + assert errors_of(record) == [] + latest = cmp.app_record(record) + assert (latest["searchDate"], latest["searchKind"]) == ( + "2026-10-22", + "prepromotion", + ) + first = cmp.app_record(record, search_kind="prepublish") + assert (first["searchDate"], first["searchKind"]) == (SEARCH_DATE, "prepublish") + assert cmp.app_record(record, search_date=SEARCH_DATE)["searchKind"] == "prepublish" + with pytest.raises(ValueError, match="no backfill search"): + cmp.app_record(record, search_kind="backfill") + + +def test_report_runs_while_gaps_are_unexplained(tmp_path, capsys, monkeypatch): + source = tmp_path / "analysis-uk-childcare-100k-threshold" + import shutil + + shutil.copytree(CHILDCARE, source) + notes = json.loads((source / "annotations.json").read_text()) + notes = [n for n in notes if n["id"] != "hours-draw-microcosm-1126"] + (source / "annotations.json").write_text(json.dumps(notes)) + monkeypatch.setattr(cmp, "SOURCES", tmp_path) + assert cmp._main(["report", source.name, "--since", "2026-10-01"]) == 0 + out = capsys.readouterr() + assert json.loads(out.out)["over_threshold"] + assert cmp.UNEXPLAINED in out.err + # any other error still refuses the record + (source / "search_log.json").write_text("{}") + assert cmp._main(["report", source.name, "--since", "2026-10-01"]) == 1 + + +def test_deployments_are_https_pages_listed_once(): + good = "https://example-analysis.vercel.app/uk/example" + assert mutate(lambda r: r.analysis.update(deployments=[good])) == [] + for bad in ( + ["http://example.vercel.app/"], + [good, good], + "x", + ["https://localhost"], + ): + assert any( + "deployments lists" in e + for e in mutate(lambda r, b=bad: r.analysis.update(deployments=b)) + ), bad + entry = json.loads(cmp.INDEX_PATH.read_text())["analyses"][CHILDCARE.name] + assert entry["deployments"] == [ + "https://uk-childcare-100k-threshold.vercel.app/uk/childcare-100k-threshold" + ] + + +def test_a_tracker_bill_is_recorded_and_due(tmp_path): + record = synthetic(-100.0, -105.0) + record.analysis["registry"]["kind"] = "tracker" + assert errors_of(record) == [] + record.analysis["registry"]["kind"] = "calculator" + assert any("registry.kind must be one of" in e for e in errors_of(record)) + directory = cmp.scaffold("us", "app", "mn-hf4621", "2026-10-20", root=tmp_path) + analysis = json.loads((directory / "analysis.json").read_text()) + analysis["registry"]["kind"] = "tracker" + analysis["published"] = "2026-10-20" + (directory / "analysis.json").write_text(json.dumps(analysis)) + rows = cmp.due([], [], "2026-10-21", 183, root=tmp_path) + assert [(r["kind"], r["source"]) for r in rows] == [ + ("tracker", "analysis-us-mn-hf4621") + ] + + def test_scaffold_lists_every_producer(tmp_path): directory = cmp.scaffold("uk", "app", "new-dashboard", SEARCH_DATE, root=tmp_path) log = json.loads((directory / "search_log.json").read_text())