diff --git a/README.md b/README.md index 489dc3b01..6fe74c379 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,17 @@ python pipeline/compute_tariff_counterparts.py # parquet if present, else the python pipeline/build_moments.py # -> app/public/data/moments.json ``` +## Pre-publish comparables + +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 Python side (requires the policyengine.py-managed environment; heavy — diff --git a/app/public/data/lanes.json b/app/public/data/lanes.json index c15152e1c..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", @@ -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 analysis searched, 22 claims, 22 PolicyEngine results beside them" + }, + { + "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 PolicyEngine results beside them" } ] } diff --git a/app/public/data/populations.json b/app/public/data/populations.json index dbe25cbcd..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": 29717, + "claims": 29739, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -105,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": 17, - "constructed": 1662, + "comparable": 50, + "concept_mismatch": 24, + "constructed": 1669, "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28945, + "same": 28967, "unknown": 269 }, "by_country": { @@ -134,9 +138,9 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29161, - "with_result": 2162, - "with_value": 1177, + "claims": 29183, + "with_result": 2184, + "with_value": 1199, "without_result": 26999, "multi_release_claims": 0 }, @@ -25512,806 +25516,1110 @@ } }, { - "claim_id": "2a18eefc3c44336f3a17", + "claim_id": "fe4ce312800fe0924aba", "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": "post_behavioural", "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%" + "estimate_kind": "central", + "fy": "2027-28", + "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": "-520m" }, "measure": null, "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.9211538461538461, + "delta": 41000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "392f8284d4c31f166150", + "claim_id": "15f8e74fa4e34a7b9554", "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%" + "estimate_kind": "central", + "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" }, "measure": null, "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\", \"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": "", - "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 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.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\", \"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": "74553c98c56cca6455f2", + "claim_id": "6ad874aab66d820fdc3b", "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": "post_behavioural", "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%" + "estimate_kind": "range_low", + "fy": "2027-28", + "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", + "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": "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.7852459016393443, + "delta": 131000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "878f5fd8884fa99dd180", + "claim_id": "0b6aa332c68212293288", "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%" + "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": "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\", \"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": "", - "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 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.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\", \"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": "8d00404bfd0eecdebbdd", + "claim_id": "283fbb3d3bf981674490", "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": "post_behavioural", "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%)" + "estimate_kind": "range_high", + "fy": "2027-28", + "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", + "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": "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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": -3.4214285714285713, + "delta": -619000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "adfa5c7278bede4c71c6", + "claim_id": "7b45daef72f83e1a2960", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "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": "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\", \"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-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 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": "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\", \"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-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 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": "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": "65b4e630a623a9d8b487", "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 2029: 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": 2029, "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_fy2028", + "basis": "post_behavioural", "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%" + "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": "78c68f7e47327f86", - "external_value": 0.23, + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.23, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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": [], + "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": "b9572e40f5a923d1494a", + "claim_id": "14d4050ab5c339dd8442", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Central Scenario, tax year 2029: 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": 2029, "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_fy2028", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "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": "7718692432a4c4f2", - "external_value": 0.004, + "reform_key": "db11caf3e07ba4d1", + "external_value": -880000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.004, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -880000000.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": -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-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 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": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.55, + "delta": 396000000.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": -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-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 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": "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": "f4397e5778474de9cc1c", "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": "post_behavioural", "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%" + "estimate_kind": "central", + "fy": "2029-30", + "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": "-640m" }, "measure": null, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.1, + "reform_key": "db11caf3e07ba4d1", + "external_value": -640000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.1, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -640000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.86875, + "delta": 84000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "ccd04b8d9b2351d508ba", + "claim_id": "5c56ff121ba5f93053f1", "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": "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_central_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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "estimate_kind": "central", + "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" }, + "measure": null, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.46, + "reform_key": "db11caf3e07ba4d1", + "external_value": -980000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.46, + "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\", \"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-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 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": "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\", \"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-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 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": "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": "f4d26f478523c790d7a0", "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: 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", - "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": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2029", + "basis": "post_behavioural", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "estimate_kind": "range_low", + "fy": "2029-30", + "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", + "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": "7718692432a4c4f2", - "external_value": 0.58, + "reform_key": "db11caf3e07ba4d1", + "external_value": -770000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.58, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -770000000.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": -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\": \"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-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" + "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": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "concept_mismatch", + "ratio": 0.7220779220779221, + "delta": 214000000.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": -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\": \"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-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" + "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": "pe_gap" + "status_effective": "concept_mismatch" } ], - "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": "dd2cde755a07c0ed9ba7", + "claim_id": "1449769add785901db25", "source": "centax", "country": "UK", - "source_column": "", + "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": "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_high_cost_fy2029", + "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, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -1010000000.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\", \"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 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.5, + "delta": 505000000.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\", \"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 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": "d8af9ff525d462679608", + "source": "centax", + "country": "UK", + "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": "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "range_high", + "fy": "2029-30", + "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", + "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, + "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": -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\": \"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": [ + "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.475, + "delta": -716000000.0 + }, + "results": [ + { + "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\": \"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": [ + "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": "53c7008af6b8a702630a", + "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", + "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, + "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\", \"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 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.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\", \"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 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": "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": "range_high", + "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "period_start": null, @@ -26322,24 +26630,23 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "estimate_kind": "range_high", + "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": "impacted farm estates valued at less than \u00a32 million", + "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": "Less than 1%" + "value_verbatim": "2%" }, "measure": null, "reform_framework": "policy_ref", "reform_key": "78c68f7e47327f86", - "external_value": 0.01, + "external_value": 0.02, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.01, + "external_value_pe_sign": 0.02, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26361,40 +26668,45 @@ "diagnosis": null }, { - "claim_id": "3de3f320ce2d052aec1c", + "claim_id": "392f8284d4c31f166150", "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", - "value_kind": "point", - "period": 2020, + "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, "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", + "estimate_kind": "range_low", "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 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%" }, "measure": null, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 1800.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.8, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 1800.0, + "external_value_pe_sign": 0.8, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26416,39 +26728,44 @@ "diagnosis": null }, { - "claim_id": "55e1fd35afbb78cc5b82", + "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": "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 farm estates", "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": "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%" }, "measure": null, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 65000.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.55, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 65000.0, + "external_value_pe_sign": 0.55, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26469,6 +26786,1394 @@ "results": [], "diagnosis": null }, + { + "claim_id": "878f5fd8884fa99dd180", + "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 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%" + }, + "measure": null, + "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%)" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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" + }, + "measure": null, + "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" + }, + "measure": null, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 65000.0, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 65000.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": "3e0f4ed92a011b5a1d9a", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "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.070" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -6.442857142857143, + "delta": -521000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "9def514bdb6ddd5f7abe", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "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.330" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3666666666666667, + "delta": -781000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "b4b2459d4a9c757a940f", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "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.077" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -6.220779220779221, + "delta": -556000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "deadc9241b4fd49010e2", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "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.349" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3724928366762177, + "delta": -828000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "9af08733f1c6826235aa", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "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.091" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -5.857142857142857, + "delta": -624000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "bf09bd19074e21cabac6", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "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.399" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3358395989974938, + "delta": -932000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "6587454d24d586b6fdf8", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "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.427" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3021077283372364, + "delta": -983000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "c7aee9e51cd5570028cc", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "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.132" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -4.212121212121212, + "delta": -688000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "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 new file mode 100644 index 000000000..d14c0fcfe --- /dev/null +++ b/data/comparables/index.json @@ -0,0 +1,847 @@ +{ + "_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, + "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", + "kind": "app", + "country_id": "uk", + "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": [ + "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", + "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": 22 + } + ], + "claims": 22, + "compared": 15, + "beside_nearest": 7, + "over_threshold": 15, + "sign_disagreements": 8, + "explained": 15, + "pending": 0, + "unbounded_gaps": 0, + "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": [ + { + "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", + "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')", + "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, + "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_fy2027", + "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 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')", + "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_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -0.0788, + "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_fy2029", + "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 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')", + "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, + "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_fy2029", + "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 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')", + "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_fy2029", + "nearest_pe_value": -556000000, + "nearest_gap": -0.1312, + "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_high_cost_fy2027", + "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 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')", + "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, + "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 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_high_cost_fy2027", + "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 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')", + "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_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -0.2148, + "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_high_cost_fy2029", + "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 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')", + "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, + "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 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_high_cost_fy2029", + "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 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')", + "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_fy2029", + "nearest_pe_value": -556000000, + "nearest_gap": -0.2779, + "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_low_cost_fy2027", + "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 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')", + "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, + "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 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_low_cost_fy2027", + "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 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')", + "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_fy2027", + "nearest_pe_value": -479000000, + "nearest_gap": -4.4214, + "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_low_cost_fy2029", + "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 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')", + "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, + "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 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_low_cost_fy2029", + "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 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')", + "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_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 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", + "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", + "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": "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", + "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", + "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": "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", + "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", + "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": "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", + "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", + "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": "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", + "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", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "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": "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", + "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", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "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": "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", + "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", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "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": "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", + "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", + "url": "https://britishprogress.org/api/media/file/rates_and_wrongs_technical_appendix.pdf", + "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": "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/data/comparables/policy.json b/data/comparables/policy.json new file mode 100644 index 000000000..f591b6e57 --- /dev/null +++ b/data/comparables/policy.json @@ -0,0 +1,10 @@ +{ + "_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", + "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/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 c15152e1c..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", @@ -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 analysis searched, 22 claims, 22 PolicyEngine results beside them" + }, + { + "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 PolicyEngine results beside them" } ] } diff --git a/data/populations.json b/data/populations.json index dbe25cbcd..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": 29717, + "claims": 29739, "multi_release_claims": 270, "by_source": { "aspe_welfare_indicators": 14, @@ -105,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": 17, - "constructed": 1662, + "comparable": 50, + "concept_mismatch": 24, + "constructed": 1669, "not_computed": 27107, "pe_gap": 889 }, "by_sign_relative_to_pe": { "magnitude": 14, "opposite": 489, - "same": 28945, + "same": 28967, "unknown": 269 }, "by_country": { @@ -134,9 +138,9 @@ "multi_release_claims": 0 }, "UK": { - "claims": 29161, - "with_result": 2162, - "with_value": 1177, + "claims": 29183, + "with_result": 2184, + "with_value": 1199, "without_result": 26999, "multi_release_claims": 0 }, @@ -25512,806 +25516,1110 @@ } }, { - "claim_id": "2a18eefc3c44336f3a17", + "claim_id": "fe4ce312800fe0924aba", "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": "post_behavioural", "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%" + "estimate_kind": "central", + "fy": "2027-28", + "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": "-520m" }, "measure": null, "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.9211538461538461, + "delta": 41000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "392f8284d4c31f166150", + "claim_id": "15f8e74fa4e34a7b9554", "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%" + "estimate_kind": "central", + "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" }, "measure": null, "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\", \"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": "", - "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 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.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\", \"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": "74553c98c56cca6455f2", + "claim_id": "6ad874aab66d820fdc3b", "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": "post_behavioural", "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%" + "estimate_kind": "range_low", + "fy": "2027-28", + "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", + "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": "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.7852459016393443, + "delta": 131000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "878f5fd8884fa99dd180", + "claim_id": "0b6aa332c68212293288", "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%" + "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": "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\", \"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": "", - "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 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.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\", \"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": "8d00404bfd0eecdebbdd", + "claim_id": "283fbb3d3bf981674490", "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": "post_behavioural", "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%)" + "estimate_kind": "range_high", + "fy": "2027-28", + "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", + "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": "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": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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": -3.4214285714285713, + "delta": -619000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "adfa5c7278bede4c71c6", + "claim_id": "7b45daef72f83e1a2960", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "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": "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\", \"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-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 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": "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\", \"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-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 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": "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": "65b4e630a623a9d8b487", "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 2029: 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": 2029, "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_fy2028", + "basis": "post_behavioural", "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%" + "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": "78c68f7e47327f86", - "external_value": 0.23, + "reform_key": "db11caf3e07ba4d1", + "external_value": -580000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.23, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -580000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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": [], + "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": "b9572e40f5a923d1494a", + "claim_id": "14d4050ab5c339dd8442", "source": "centax", "country": "UK", - "source_column": "", - "name": "Equalising National Insurance on Partnership Income: Revenue and Distributional Effects", + "source_column": "Central Scenario, tax year 2029: 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": 2029, "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_fy2028", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "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": "7718692432a4c4f2", - "external_value": 0.004, + "reform_key": "db11caf3e07ba4d1", + "external_value": -880000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.004, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -880000000.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": -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-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 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": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "constructed", + "ratio": 0.55, + "delta": 396000000.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": -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-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 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": "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": "f4397e5778474de9cc1c", "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": "post_behavioural", "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%" + "estimate_kind": "central", + "fy": "2029-30", + "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": "-640m" }, "measure": null, "reform_framework": "policy_ref", - "reform_key": "78c68f7e47327f86", - "external_value": 0.1, + "reform_key": "db11caf3e07ba4d1", + "external_value": -640000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.1, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -640000000.0, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { - "value": null, - "status": "not_computed", - "engine_version": "", - "data_bundle": "", - "release": "", - "construction": "", + "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\": \"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": "", - "annotations": [], - "baseline": null, - "status_effective": "not_computed", - "ratio": null, - "delta": null + "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.86875, + "delta": 84000000.0 }, - "results": [], + "results": [ + { + "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\": \"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": [ + "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": "ccd04b8d9b2351d508ba", + "claim_id": "5c56ff121ba5f93053f1", "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": "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_central_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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "estimate_kind": "central", + "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" }, + "measure": null, "reform_framework": "policy_ref", - "reform_key": "7718692432a4c4f2", - "external_value": 0.46, + "reform_key": "db11caf3e07ba4d1", + "external_value": -980000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.46, + "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\", \"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-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 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": "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\", \"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-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 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": "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": "f4d26f478523c790d7a0", "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: 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", - "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": { - "action_link": "https://github.com/PolicyEngine/policyengine-scorecard/issues/136", - "basis": "static", + "analysis": "uk/childcare-100k-threshold", + "analysis_claim": "centax_net_high_cost_fy2029", + "basis": "post_behavioural", "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%" - }, - "measure": { - "key": "ab2025_option__nics_on_partnership_income", - "program": "national_insurance", - "computability": "not_expressible", - "head_variables": [], - "parameter_paths": [] + "estimate_kind": "range_low", + "fy": "2029-30", + "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", + "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": "7718692432a4c4f2", - "external_value": 0.58, + "reform_key": "db11caf3e07ba4d1", + "external_value": -770000000.0, "sign_relative_to_pe": "same", - "sign_basis": "level", - "external_value_pe_sign": 0.58, + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -770000000.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": -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\": \"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-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" + "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": "pe_gap", - "ratio": null, - "delta": null + "status_effective": "concept_mismatch", + "ratio": 0.7220779220779221, + "delta": 214000000.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": -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\": \"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-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" + "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": "pe_gap" + "status_effective": "concept_mismatch" } ], - "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": "dd2cde755a07c0ed9ba7", + "claim_id": "1449769add785901db25", "source": "centax", "country": "UK", - "source_column": "", + "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": "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_high_cost_fy2029", + "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, + "sign_relative_to_pe": "same", + "sign_basis": "stated:exchequer:same", + "external_value_pe_sign": -1010000000.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\", \"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 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.5, + "delta": 505000000.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\", \"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 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": "d8af9ff525d462679608", + "source": "centax", + "country": "UK", + "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": "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "estimate_kind": "range_high", + "fy": "2029-30", + "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", + "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, + "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": -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\": \"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": [ + "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.475, + "delta": -716000000.0 + }, + "results": [ + { + "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\": \"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": [ + "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": "53c7008af6b8a702630a", + "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", + "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, + "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\", \"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 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.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\", \"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 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": "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": "range_high", + "value_kind": "point", "period": 2027, "time_basis": "fiscal_year", "period_start": null, @@ -26322,24 +26630,23 @@ "basis": "static", "benchmark_class": "different_model", "country": "UK", - "denominator": "additional tax from farm estates", - "estimate_kind": "range_high", + "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": "impacted farm estates valued at less than \u00a32 million", + "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": "Less than 1%" + "value_verbatim": "2%" }, "measure": null, "reform_framework": "policy_ref", "reform_key": "78c68f7e47327f86", - "external_value": 0.01, + "external_value": 0.02, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 0.01, + "external_value_pe_sign": 0.02, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26361,40 +26668,45 @@ "diagnosis": null }, { - "claim_id": "3de3f320ce2d052aec1c", + "claim_id": "392f8284d4c31f166150", "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", - "value_kind": "point", - "period": 2020, + "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, "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", + "estimate_kind": "range_low", "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 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%" }, "measure": null, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 1800.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.8, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 1800.0, + "external_value_pe_sign": 0.8, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26416,39 +26728,44 @@ "diagnosis": null }, { - "claim_id": "55e1fd35afbb78cc5b82", + "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": "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 farm estates", "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": "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%" }, "measure": null, - "reform_framework": "baseline", - "reform_key": "03395a10966bb849", - "external_value": 65000.0, + "reform_framework": "policy_ref", + "reform_key": "78c68f7e47327f86", + "external_value": 0.55, "sign_relative_to_pe": "same", "sign_basis": "level", - "external_value_pe_sign": 65000.0, + "external_value_pe_sign": 0.55, "calibration_relationship": "held_out", "claim_baseline": "current_law", "latest": { @@ -26469,6 +26786,1394 @@ "results": [], "diagnosis": null }, + { + "claim_id": "878f5fd8884fa99dd180", + "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 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%" + }, + "measure": null, + "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%)" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": { + "key": "ab2025_option__nics_on_partnership_income", + "program": "national_insurance", + "computability": "not_expressible", + "head_variables": [], + "parameter_paths": [] + }, + "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%" + }, + "measure": null, + "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" + }, + "measure": null, + "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" + }, + "measure": null, + "reform_framework": "baseline", + "reform_key": "03395a10966bb849", + "external_value": 65000.0, + "sign_relative_to_pe": "same", + "sign_basis": "level", + "external_value_pe_sign": 65000.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": "3e0f4ed92a011b5a1d9a", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "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.070" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -6.442857142857143, + "delta": -521000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "9def514bdb6ddd5f7abe", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2026-27", + "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.330" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3666666666666667, + "delta": -781000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "b4b2459d4a9c757a940f", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "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.077" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -6.220779220779221, + "delta": -556000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "deadc9241b4fd49010e2", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2027-28", + "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.349" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3724928366762177, + "delta": -828000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "9af08733f1c6826235aa", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "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.091" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -5.857142857142857, + "delta": -624000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "bf09bd19074e21cabac6", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2028-29", + "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.399" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3358395989974938, + "delta": -932000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "6587454d24d586b6fdf8", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "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.427" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -1.3021077283372364, + "delta": -983000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, + { + "claim_id": "c7aee9e51cd5570028cc", + "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": "post_behavioural", + "benchmark_class": "different_model", + "country": "UK", + "fy": "2029-30", + "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.132" + }, + "measure": null, + "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": -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\": 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": [ + "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": "comparable", + "ratio": -4.212121212121212, + "delta": -688000000.0 + }, + "results": [ + { + "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\": 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": [ + "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": "comparable" + } + ], + "diagnosis": null + }, { "claim_id": "03d666ddda6fe83bd6f4", "source": "co_admin", diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8398757ee..b4ed3a2c1 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -205,6 +205,18 @@ 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 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 Administrative outturn tables (caseloads, units, spending) are calibration diff --git a/docs/COMPARABLES.md b/docs/COMPARABLES.md new file mode 100644 index 000000000..b84b9c4b1 --- /dev/null +++ b/docs/COMPARABLES.md @@ -0,0 +1,273 @@ +# 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 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 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 + (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** (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 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 is the + analysis's first search if it has none yet (`backfill`). +- 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 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, 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, 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 | + +Start one with: + +```bash +PYTHONPATH=. python -m scorecard_db.comparables scaffold --country uk --kind app --slug +``` + +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 +`time_basis`, and two conditions: + +- `conditions.basis`: `static`, `post_behavioural`, `forecast` or `outturn`; +- `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). + +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 +`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. + +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 +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). + +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 +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 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`, `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. +- **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. 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`, 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`. 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 + +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 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 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 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. + +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 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 + +`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- +``` + +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", + "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" +} +``` + +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 + +```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 +``` + +`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/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..b998a0a13 --- /dev/null +++ b/scorecard_db/comparables.py @@ -0,0 +1,2217 @@ +"""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--/``: + + 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, 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: 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 + +import argparse +import datetime as dt +import hashlib +import json +import math +import re +import sys +from collections import defaultdict +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-" + +# 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") +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. +RESULTS = ("found", "none_found", "found_not_staged", "not_applicable") +VALUE_KINDS = ( + "point", + "central", + "range_low", + "range_high", + "approx_chart_reading", + "cumulative", + "lower_bound", + "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. +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") +BASIS_KINDS = ("issue", "document", "engine_metadata", "diagnostic") +# 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 = ( + ComparisonStatus.COMPARABLE.value, + ComparisonStatus.CONSTRUCTED.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'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", "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 +# 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})?)?$" +) +_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_.-]+$") +_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"(#[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+$") +# 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 +# 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, 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 = "-−–‒-" +_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, + "£(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. + """ + match = _number_match(value_raw) + text = value_raw.strip() + 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 + + +_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". 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 + for match in re.finditer(rf"(? list[str]: + """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 + 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 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: + 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)})" + ) + ] + 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}"] + if not math.isfinite(value): + 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}"] + 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} " + f"under {normalization!r}{' with sign -1' if sign == -1 else ''} " + f"(expected {expected!r})" + ) + return errors + + +# --- 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") +# 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]: + """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. + + 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" + 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, + "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, 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: + 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 + + +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 = _loads(path.read_text()) + threshold = policy.get("gap_threshold") + # 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 number between 0 and 1: {threshold!r}" + ) + # 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 0 < days <= most + ): + raise ValueError( + f"{path}: {key} must be a whole number of days from 1 to {most}: {days!r}" + ) + return policy + + +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_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']} added must be a real date, no later than today" + ) + 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 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"] + + +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(_loads(line)) + except (ValueError, 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 _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) + ) + + +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 -------------------------------------------------------------- + + +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 _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) 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_file(ref): + return [] + return [ + ( + f"{where}: engine metadata is a file pinned to a commit " + f"(github.com///blob/<40-hex>/) or a file in this repo: {ref!r}" + ) + ] + if _URL_WITH_PATH.match(ref) or _repo_file(ref): + return [] + 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, bases: tuple = BASES) -> list[str]: + """The conditions that, with metric, unit, period, time basis and world, + 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 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) + 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( + _condition(claim, f) == _condition(estimate, 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: + 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 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(): + 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") + 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") + 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 (_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") + 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") + 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 []): + 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" + ) + continue + estimates[key] = e + errors.extend(_check_estimate(e, f"{name}: estimate {key}", world_keys)) + # 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; 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 []): + 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] = {} + 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) + 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 _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: + 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}: 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 job that ran it") + pins = s.get("results_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)" + ) + 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] + 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: + 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", []) + 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 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": + 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_) + 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 ---------------------------------------------------------------------- + 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}" + 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 in BANNED_SOURCES: + errors.append( + f"{where}: source names the producer that published the figure " + f"(not {source!r})" + ) + else: + belongs = producer_for_source(producers, country, source) + if belongs is not None and belongs != found_by: + errors.append( + 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" + ) + 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" + ) + 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") + 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 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.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") + 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", "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" + ) + 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") + 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'") + 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") + near = sorted(k for k, e in estimates.items() if _near(c, e)) + if near and nearest not in near: + errors.append( + 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 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 UNCOMPARED_KINDS + 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") + 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')})" + ) + if value_kind in UNCOMPARED_KINDS: + errors.append( + 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") + 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" + ) + 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" + ) + for f in IDENTITY_CONDITIONS: + 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}; " + "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 ({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") + # 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 and m.get("url") not in linked: + errors.append( + 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) + 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( + _condition(low, f) != _condition(high, f) for f in IDENTITY_CONDITIONS + ): + errors.append( + f"{name}: range_group {group}'s bounds measure different things" + ) + + # 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)") + 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" + ) + 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") + 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} 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: + if key not in annotated: + errors.append( + f"{name}: claim {key} sits beside its nearest estimate uncompared, so an " + f"annotation with status {' or '.join(MISMATCH_STATUSES)} must say why" + ) + + # the gate ------------------------------------------------------------------------ + if errors: + return errors + for row in evaluate(record, policy): + 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 {shown} from {row['estimate']}" + f"{' (its sign disagrees)' if row['sign_disagrees'] else ''} " + f"(threshold {policy['gap_threshold']:.0%}) and {UNEXPLAINED}" + ) + 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, 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"] + 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, + "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"]] + 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 = ( + ComparisonStatus.CONSTRUCTED.value + if c["world"] != est["world"] + else ComparisonStatus.COMPARABLE.value + ) + row.update( + estimate=est["key"], + relation=relation, + gap=gap, + 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 + + +# --- 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"], + "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"]), + "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 or 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"], + "deployments": a.get("deployments", []), + "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"]} + ), + "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 (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} + out = [] + 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"], + "found_on": c["found_on"], + "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"), + "basis": conditions.get("basis"), + "program": conditions.get("program"), + "period": c["period"], + "fy": conditions.get("fy"), + "value_kind": c["value_kind"], + "range_group": row["range_group"], + "value": c["value"], + "value_raw": c["value_raw"], + "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"), + } + ) + return out + + +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 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"], + "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, + } + + +def render_index(index: dict) -> str: + # 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 ------------------------------------------------------------ + + +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 {search_kind or ''} search" + f"{' on ' + search_date if search_date else ''}".replace(" ", " ") + ) + 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() + if country not in producers["countries"]: + raise SystemExit( + 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, + "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": "", + "results_pins": [], + "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 + + +# --- 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 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 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("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_ids": [app["countryId"]], + "slug": app["slug"], + "date": (app.get("date") or "")[:10] or None, + "title": app.get("title", ""), + "authors": app.get("authors", []), + "tags": tags, + "comparables": app.get("comparables"), + } + ) + for post in posts: + 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], + today: str, + window_days: int, + root: Path = SOURCES, +) -> list[dict]: + """Research entries published within the re-harvest window that publish an + 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 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] + ) + 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"}, + "source": name, + "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"])) + + +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. 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 + ) + 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.get("status") == "pending" and c.get("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.""" + 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", + "group_gap", + "sign_disagrees", + "status", + "annotation", + "tracking_issue", + ) + } + for r in rows + if r["over"] + ], + } + + +# --- CLI ------------------------------------------------------------------------------ + + +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_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=_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_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) + 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_utc()) + 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 == "pins": + record = load_analysis(SOURCES / args.source) + print(json.dumps(current_pins(record.analysis), indent=1)) + return 0 + if args.cmd in ("record", "report", "fresh"): + 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, args.search_kind), + 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 + + +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/export_populations.py b/scorecard_db/export_populations.py index 385131779..8f4201dce 100644 --- a/scorecard_db/export_populations.py +++ b/scorecard_db/export_populations.py @@ -227,6 +227,7 @@ def export( measures = uk_measures() 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 @@ -297,7 +298,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 @@ -440,6 +449,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 new file mode 100644 index 000000000..165e92e43 --- /dev/null +++ b/scorecard_db/ingest_analysis_comparables.py @@ -0,0 +1,342 @@ +"""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 +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; +- 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 +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 _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 + 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"], + # 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"], + "value_verbatim": c["value_raw"], + } + 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 = { + **{ + 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 row["estimate"] and not row["nearest"]: + 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=_value_kind(c["unit_concept"]), + status="ok", + ) + scores.append(score) + t["claims"] += 1 + if not row["estimate"] and not row["nearest"]: + t["not_compared"] += 1 + continue + # 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']}") + if status == "pending": + # 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 + 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 + 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"{_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) + 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/README.md b/sources/analysis-uk-childcare-100k-threshold/README.md new file mode 100644 index 000000000..43a33eb89 --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/README.md @@ -0,0 +1,30 @@ +# £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, + 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 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 +PolicyEngine's certified-pair counterpart. 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..3615eb1fd --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/analysis.json @@ -0,0 +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", + "deployments": [ + "https://uk-childcare-100k-threshold.vercel.app/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." + }, + "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" + } + }, + { + "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_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", + "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_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", + "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_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", + "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" + } + }, + { + "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 new file mode 100644 index 000000000..d2a7205ac --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/annotations.json @@ -0,0 +1,90 @@ +[ + { + "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", + "centax_static_central_fy2028" + ], + "status": "constructed", + "explains_gap": true, + "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", + "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#issuecomment-6337369324" + } + ] + }, + { + "id": "centax-net-vs-dashboard-dynamic", + "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", + "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", + "cbp_net_table2_fy2029", + "cbp_net_table3_fy2026", + "cbp_net_table3_fy2027", + "cbp_net_table3_fy2028", + "cbp_net_table3_fy2029" + ], + "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://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=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 new file mode 100644 index 000000000..ad42fa4ba --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/claims_staged.jsonl @@ -0,0 +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": "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/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..79de38fab --- /dev/null +++ b/sources/analysis-uk-childcare-100k-threshold/search_log.json @@ -0,0 +1,255 @@ +{ + "schema": "comparables/search/1", + "searches": [ + { + "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", + "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", + "centax_static_central_fy2028", + "centax_net_central_fy2028" + ], + "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", + "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/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 + } +] 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", ) diff --git a/tests/test_comparables.py b/tests/test_comparables.py new file mode 100644 index 000000000..87583b242 --- /dev/null +++ b/tests/test_comparables.py @@ -0,0 +1,1679 @@ +"""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 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, 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; 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 +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" +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 = { + "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", + }, +} + + +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 _producers(found_by: str, keys: list[str]) -> list[dict]: + out = [] + 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"] == found_by: + entry.update(result="found", claims=list(keys)) + if not cmp.applies(p, "uk_wide"): + entry.update(result="not_applicable", queries=[]) + 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", + "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": {**identity, "sign_convention": 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", + }, + } + ], + } + printed = ("-" if ext_value < 0 else "") + 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": printed, + "normalization": "as printed", + "value_kind": "point", + "period": 2028, + "time_basis": "fiscal_year", + "conditions": {**identity, "sign_convention": 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 {printed}", + "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", + "results_pins": [PIN], + "producers": _producers("ifs", [claim_key]), + } + ], + }, + 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) + + +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 --------------------------------------------- + + +@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 + 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")}) + json.loads(cmp.INDEX_PATH.read_text(), parse_constant=lambda c: pytest.fail(c)) + + +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_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, "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 = [ + 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_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 + ) + # 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" + # 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["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 ------------------------------------------------------------- + +money = st.floats(min_value=1e3, max_value=1e12, allow_nan=False, allow_infinity=False) + + +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(_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] == [] + 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, 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(): + 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)) + + +# --- no evasion: relabelling a like-for-like claim never hides its gap --------------- + + +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) + + +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" + ), + "price base relabelled": lambda r: claim0(r)["conditions"].update(prices="real"), + "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"), + "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) + ), + "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" + ), +} + + +@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" + + +# --- search integrity ----------------------------------------------------------------- + + +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) + + +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" + + errors = mutate(f) + assert any("before the UK producer list" in e for e in errors) + + +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_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)) + + def g(r): + r.search_log["searches"][0]["kind"] = "backfill" + r.analysis["published"] = "2026-10-22" + + errors = mutate(g) + assert any("after publication" in e or "no later than today" in e for e in errors) + + +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)) + + +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_backfill_find_may_wait_on_its_issue(): + def f(r): + r.analysis["published"] = "2026-10-12" + r.search_log["searches"][0]["kind"] = "backfill" + + 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_a_new_results_file_needs_a_new_search(): + def f(r): + r.analysis["pe_estimates"][0]["provenance"]["commit"] = "c" * 40 + + errors = mutate(f) + assert any("a new results file needs a new search" in e for e in errors) + + +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) == [] + + +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): + 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) + + +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(): + 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(): + errors = mutate(lambda r: _search(r)[0].update(queries=[], checked=[])) + assert any("no query run" in e for e in errors) + + +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(): + 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) + + +def test_a_claim_no_search_found_fails(): + def f(r): + for p in _search(r): + 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: 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 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)) + assert any("is not value_raw" in e for e in errors) + + +def test_a_quote_without_the_value_fails(): + 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") + # 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(): + 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", + } + claim0(r)["world"] = "other" + + assert any("compare across worlds only with" in e for e in mutate(f)) + + +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_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_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_nearest_estimate_must_be_near(): + def f(r): + claim0(r).update( + compare_to=None, + pe_missing="a different period entirely", + period=2031, + nearest_estimate="pe_cost_fy2027", + ) + claim0(r)["conditions"].pop("fy") + + assert any("is not near this claim" in e for e in mutate(f)) + + +def test_a_claim_beside_its_nearest_estimate_needs_a_cited_mismatch(): + def name_it(r): + 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( + note("net-vs-static", ["ext_cost_fy2027"], "concept_mismatch") + ) + + assert mutate(annotate) == [] + + +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_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"}], + } + ) + + 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( + 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( + {"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" + ) + + +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)) + + +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) + + +# --- 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, 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 + + +@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" + path.write_text(json.dumps(dict(POLICY, gap_threshold=bad))) + with pytest.raises(ValueError, match="between 0 and 1"): + cmp.load_policy(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=days))) + 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"} + + +@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 ------------------------------------------------------------------------ + + +@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"] and s["kind"] == block["searchKind"] + ) + assert block["scorecardSource"] == directory.name + assert block["producersSearched"] == search["producers_searched"] + assert block["found"] == search["found"] + 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_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()) + 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_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", + "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": "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 --------------------------------------------------------------------------- + + +@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_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): + 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 _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"],), + ).fetchall() + 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"] + assert seen == set(expected) + + +def test_childcare_result_is_the_published_figure(db): + row = db.execute( + "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" diff --git a/tests/test_export_populations.py b/tests/test_export_populations.py index c5b38a696..1ee5f2616 100644 --- a/tests/test_export_populations.py +++ b/tests/test_export_populations.py @@ -407,7 +407,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 + 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"} @@ -442,7 +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. - assert len(payload["rows"]) == 5719 + # 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()