NICs salary sacrifice cap: carried, scoreable, and not assumption-neutral (#109) - #110
Open
vahid-ahmadi wants to merge 1 commit into
Open
NICs salary sacrifice cap: carried, scoreable, and not assumption-neutral (#109)#110vahid-ahmadi wants to merge 1 commit into
vahid-ahmadi wants to merge 1 commit into
Conversation
…tral (#109) The £2,000 cap on NI relief for salary-sacrificed pension contributions is the Budget's largest NICs measure, and the certified engine already carries it: gov.hmrc.national_insurance.salary_sacrifice_pension_cap 2026-2028 uncapped 2029+ 2000 That answers the #99/#101 baseline-integrity question for this measure: carried, dated step in the right place, correct gov.uk references. Checked and NOT a finding: the step is dated 2029-01-01 while the measure starts April 2029. Every parameter under gov.hmrc.national_insurance uses 01-01 — module-wide convention, not an anomaly. Recorded so nobody re-raises it as a defect. The actual finding is what a DEFAULT run of this measure assumes: salary_sacrifice_broad_base_haircut_rate 0.0016 ON employee_salary_sacrifice_reduction_rate 0 OFF The haircut reduces employment income for ALL workers, not only sacrificers, on the reasoning that employers spread the added NI cost across the whole workforce. It is gated on the cap being finite, so it switches on exactly when the measure bites, and it cites a PolicyEngine research page as its methodology. The employee response — people reducing salary sacrifice once capped — is off. So the default carries a PolicyEngine incidence assumption that is neither HMRC's nor OBR's, and the two assumptions point opposite ways. This is the #97 pattern sharpened: there the default asserted NO behaviour, so a static leg was honest unqualified; here the default asserts some. The validator therefore enforces DISCLOSURE, not only citation: - a leg running the haircut may be publishable, but only while marked disclosure_required — otherwise it publishes an incidence assumption as if it were arithmetic - an assumption-free leg must exist and be publishable, or a static official costing has no comparable PE leg - the sweep is refused if the haircut default ever reads zero, or if the asymmetry disappears: both would mean this lane's finding no longer holds, and failing beats continuing to assert it - the schedule must show BOTH states and must never lapse - no staged value claims: IFS's assessment and the official costing belong to their originators (#86/#91) No counterpart is computed here — UK compute runs through #51. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016HuXJFVme8HRbnke2Ey3Me
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.
Closes #109. Branches from
main— independent of the other UK PRs.NICs is one of the best-modelled areas of
policyengine-uk(class 1/2/3/4, employer and employee legs, salary sacrifice), the Budget's biggest NICs measure is already in the certified baseline, and no scorecard lane touched any of it.The measure is carried
gov.hmrc.national_insurance.salary_sacrifice_pension_cap:Correct gov.uk references on the parameter. That answers the #99/#101 baseline-integrity question for this measure: carried, with the dated step in the right place.
Checked and not a finding: the step is dated
2029-01-01while the measure starts April 2029. Every parameter undergov.hmrc.national_insuranceuses01-01— module-wide convention, not an anomaly. Recorded in the spec so nobody re-raises it as a defect later.The finding
A default PE-UK score of this measure is not assumption-neutral, and the two assumptions point opposite ways:
salary_sacrifice_broad_base_haircut_rateemployee_salary_sacrifice_reduction_ratesalary_sacrifice_broad_base_haircutreduces employment income for all workers — not just sacrificers — on the reasoning that employers spread the added NI cost across the whole workforce. It's gated on the cap being finite (if np.isinf(cap): return 0), so it switches on exactly when the measure bites, and it cites a PolicyEngine research page as its methodology. Meanwhile the employee response — people reducing salary sacrifice once it's capped — is switched off.So the default carries a PolicyEngine incidence assumption that is neither HMRC's nor OBR's. Published without disclosure, it shows up in a comparison as arithmetic disagreement.
This is #97 sharpened. There, the engine's default asserted no behaviour, so a static leg was honest without qualification. Here the default asserts some.
So the validator enforces disclosure, not just citation
disclosure_required— otherwise it publishes an incidence assumption as if it were arithmetic.Three divergence axes are named up front (#59): broad-base incidence, employee response, and employer-NI incidence (PE assumes full employee incidence, 1.0).
Not computed here
Specifies and resolves; computes no counterpart. UK compute runs through #51.
Gates
282 tests (19 new),
ruff format --checkclean, twobuild_dbruns agree oncontent_hash, no drift.