Repository navigation
Conversation
The aggregate impact report (#2335) adds gov_spending, household_benefits and spending_heads to fiscal_partition.yaml. The tax tests now check the three tax totals only, require that spending heads borrow no tax head id or variable and that no spending head names a tax total, and apply dropped_when to the tax totals alone. Checked against #2335's file with these tax sections merged in: this file's tests and #2335's partition and guard tests pass. The vat head records that 76 households have negative VAT on policyengine-uk-data 1.58.0 (consumption or the full-rate expenditure share is negative in the data; policyengine-uk-data issue 565). Refs #2159 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finance Act 2025 s.47 takes private school education, vocational training and closely related board and lodging out of the VATA 1994 Sch 9 Group 6 exemption, with effect for supplies on or after 1 January 2025 (s.49). The new gov.hmrc.vat.private_school_fees_exempt parameter records the exemption; when it is off the fees bear the standard rate. The contributed reform rate now raises the rate instead of adding to it, so setting it never charges the fees twice. private_school_vat moves to variables/gov/hmrc. Household taxes include it as a change from baseline (private_school_vat_change), as they do VAT; government tax revenue includes the level. Fixes #2318 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
private_school_vat is now non-zero at current law, so it leaves the zero-at-baseline heads and stays in gov_tax only; household_tax sums the new private_school_vat_change head, which is zero at baseline, with baseline_private_school_vat as its baseline measure. A level-and-change guard mirrors vat and vat_change. The vat overlap guard becomes a disjointness property: vat's full-rate share was trained when the fees were exempt, and neither head reads the other's inputs or parameters. Adds Hypothesis properties: private_school_vat matches the statute computed independently, a reform rate at or below the statutory rate never taxes the fees twice, the head is monotone in both rates, vat and private_school_vat are disjoint, and the level, baseline and change agree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
baseline_private_school_vat now applies baseline policy (the exemption, the standard rate and the reform rate) to this simulation's attendance, mean fee and share of fees bearing VAT, so a reform that changes only those assumptions (such as disabling economic assumptions, which moves the fee uprating) is no longer scored as a household tax change. YAML cases now compute the change and include reforms that raise and remove it; a grid test checks the statutory rate exhaustively across the commencement, the exemption, reform rates and standard rates. The exemption parameter starts when VAT did (Finance Act 1972 Sch 5 Group 6), so no earlier lookup reads as taxable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The vat guard becomes overlapping_base: the heads read separate inputs, but vat applies one household share to all consumption including education, so a fee-paying household's fees are charged at its blended share, and in a household calculation education consumption entered as an input is charged in both heads. On calibrated data the overlap is in aggregate, because policyengine-uk-data targets vat at total OBR VAT receipts (policyengine-uk-data#567). The partition header lists the guard kinds and says data overlaps are recorded under their guards. The identity property now draws households with a child at private school and a scenario restoring the exemption, and checks the head is positive exactly where a pupil's fees are charged. The changelog says contributed reforms at 20% now score nothing from 2025. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
fix/private-school-vat-current-law
branch
from
October 11, 2026 18:56
a68673d to
ccbab58
Compare
From the second independent review: - The identity property always includes a household with a pupil, and under a restored exemption checks the head is zero and the change is minus the baseline charge exactly where a pupil's fees bore it. - The grid's reference uses the statutory 20 per cent, not the model's parameter. - The eligibility-reads guard says its check is vacuous for the private school heads on situations; the header says formula overlaps between heads with unseparated bases are recorded under their guards. - private_school_vat notes that every imputed pupil is charged (nursery classes excepted by law, 18-year-olds chargeable), and the baseline notes it reads 1 January values where the level reads 30 April. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
force-pushed
the
fix/corporate-sdlt-double-count
branch
from
October 11, 2026 23:32
5f08c24 to
47eb1f7
Compare
33 of 69 tasks
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2318
Summary
VAT has applied to private school fees since 1 January 2025, but
private_school_vatwas £0 at current law: its only rate was the contributed reform parametergov.contrib.labour.private_school_vat, which is 0 for all time.This PR adds the statutory charge and keeps the contributed rate for reforms without letting it tax the fees twice.
Household taxes now include private school VAT as a change from baseline, as they do VAT, so baseline household net income is unchanged and reforms move it.
Stacked on #2235, which adds
policyengine_uk/data/fiscal_partition.yaml. The base is #2235's branch, so this diff shows only this change. It retargets to main when #2235 merges.The law
Before. VATA 1994 Sch 9 Group 6 item 1 exempts "the provision by an eligible body of education", and Note (1)(a)(i) makes a registered independent school an eligible body (as enacted). The exemption dates from VAT's start: Finance Act 1972 Sch 5 Group 6 item 1, "the provision of education if it is provided by a school" (as enacted).
Finance Act 2025 s.47 (enacted) inserts Sch 9 Part 3, items 1 to 3:
It amends s.31(1), so a supply is exempt only if it is in Part 2 and not in Part 3. No zero or reduced rate applies, so these supplies bear the standard rate.
Section 49 (enacted) gives effect "in relation to any provision of education, vocational training or board and lodging on or after 1 January 2025".
Does
vatalready tax the fees?Not by formula. But the two bases are not separated household by household, and on calibrated data they overlap in aggregate. What the code does:
vat= (full_rate_vat_consumption× standard rate +reduced_rate_vat_consumption× reduced rate) ÷microdata_vat_coverage(0.38).full_rate_vat_consumption=consumption×full_rate_vat_expenditure_rate, andconsumptionadds the twelve COICOP categories, includingeducation_consumption(LCFS P610).policyengine-uk-data (tag 1.58.0) imputes
full_rate_vat_expenditure_rate:totvatandexpdisfor training year 2023, that is FYE 2024 (datasets/imputations/vat.py,datasets/private_releases.py);Private school fees were exempt in FYE 2024, so the share prices them as exempt in aggregate. A fee-paying household's fees are charged at its blended share.
Household calculator. With the default share of 0.5, education consumption entered as an input is charged in
vatas well as inprivate_school_vat, where attendance is an input.Calibrated data. policyengine-uk-data calibrates weighted
vatto the OBR "Value added tax" receipts line (targets/sources/obr.py, calibration year 2025). That line includes VAT on private school fees from 2025-26, sogov_taxcounts it in both heads until the target nets it out: OBR VAT target includes VAT on private school fees, which policyengine-uk now models separately policyengine-uk-data#567.private_school_vatitself charges the fees once, at max(statutory rate, reform rate). Neither head reads the other's inputs or parameters (tested).The change
gov.hmrc.vat.private_school_fees_exempt: true from 1973-04-01, false from 2025-01-01. When false, the fees beargov.hmrc.vat.standard_rate.gov.contrib.labour.private_school_vatgov.contrib.labour.private_school_vat)private_school_vatvariables/contrib/labour/variables/gov/hmrc/; formula pupils × mean fee × share of fees bearing VAT × rate, unchanged otherwisehousehold_taxprivate_school_vatprivate_school_vat_change=private_school_vat−baseline_private_school_vatgov_taxprivate_school_vatThe baseline.
baseline_private_school_vatapplies baseline policy (the exemption, the standard rate, the reform rate) to this simulation's attendance, mean fee and share. So a reform that only changes those assumptions is not scored as a tax change; one example is disabling economic assumptions, which moves the fee uprating.Why a boolean. The exemption is a boolean, not a 20% rate parameter, because the law removes an exemption and the standard rate follows: a reform to the standard rate moves the fees with it.
The contributed rate. Taking the maximum means:
Repealing the charge is
gov.hmrc.vat.private_school_fees_exempt: true.Fiscal partition (
policyengine_uk/data/fiscal_partition.yaml, from #2235)private_school_vatdropszero_at_baselineand is ingov_taxonly.private_school_vat_changeinhousehold_tax:sign: any,zero_at_baseline: expected. It declares its reads: the level, and the attendance imputation's routes to income tax, NI and council tax.baseline_private_school_vat(baseline_measure).private_school_vat_level_and_change(level_and_change).overlapping_baseand says what is above: separate inputs, overlapping base, the calculator double charge, and the calibrated-data overlap with a link to policyengine-uk-data#567. The header lists the guard kinds.test_fiscal_partition.py's identity property now:private_school_vat > 0exactly where a pupil's fees are charged.Invariants
For every household and parameter set:
private_school_vat= pupils × fee × share × max(statutory, reform), where statutory is 0 while exempt and the standard rate otherwise. The reference is computed in the test from the statute, not from the formula.private_school_vatunchanged.private_school_vatnever falls when the reform rate or the standard rate rises.vatis unchanged by any private school parameter.private_school_vatis unchanged by consumption, including education consumption.Tests
tests/policy/baseline/gov/hmrc/private_school_vat.yamlgov_taxandhousehold_tax, including a reform that raises the change (+500) and one that removes it (−2,000)tests/test_private_school_vat_properties.pytests/test_fiscal_partition.pytests/policy/baseline/gov/hmrc/excise_integration.yamlMutations, run locally:
household_taxsumming the levelLocal runs: 15 + 2 YAML cases, 53 property and grid tests, and 40 partition tests pass. CI does not run on a PR whose base is another branch, so it runs when this retargets to main.
National impact (real runs)
Running: policyengine-uk-data 1.58.0 certified
enhanced_frs_2024_25.h5, base origin/main vs head (main plus this PR's model files), policyengine-core 3.33.0, 2025-26 to 2030-31. Four scenarios: current law, the reform rate at 20%, the exemption restored, and a basic rate of 21% to measure attendance re-imputation. This section will be filled from those runs.Known limitations (tracked)
private_school_vat.pyrecords the simplification. Restricting only the charge would cut a calibrated total that sits close to the real school-age count. The fix is to impute over school ages and re-solve the factor: Private school attendance: store the imputation, impute over school ages, follow the pupil trend policyengine-uk-data#568, point 2. The runs report pupils by age.gov_taxthroughprivate_school_vatbut nothousehold_tax. The runs measure this for a basic rate of 21%. The durable fix is to store the imputed attendance in the dataset: policyengine-uk-data#568, point 1.Downstream
gov.contrib.labour.private_school_vatto 0.2 for 2025 onwards now score nothing; the changelog says so.private_school_vat_20pctis built as a forward delta on that parameter. Its identical-worlds guard will flag the event once it pins a release with this change. A follow-up rebuilds it as a reverse delta.Review
Both rounds ran on
subfleet run --task review --tier hard. Opus 5.5 lanes served them, because the Codex lanes were full.Round 1 (
review_r1.md) asked for changes, and every finding is addressed or disclosed:Round 2 (Opus 5.5 lane,
review_r2.md): APPROVE. The small follow-ups are done:parameters.baselinereads 1 January values;axiom: TheAxiomFoundation/rulespec-uk#453 queued
🤖 Generated with Claude Code