Skip to content

NICs salary sacrifice cap: carried, scoreable, and not assumption-neutral (#109) - #110

Open
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/nics-salary-sacrifice-lane
Open

NICs salary sacrifice cap: carried, scoreable, and not assumption-neutral (#109)#110
vahid-ahmadi wants to merge 1 commit into
mainfrom
uk/nics-salary-sacrifice-lane

Conversation

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

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:

year value
2026–2028 uncapped
2029 onward £2,000

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-01 while the measure starts April 2029. Every parameter under gov.hmrc.national_insurance uses 01-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:

parameter default
salary_sacrifice_broad_base_haircut_rate 0.0016 ON
employee_salary_sacrifice_reduction_rate 0 OFF

salary_sacrifice_broad_base_haircut reduces 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

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 --check clean, two build_db runs agree on content_hash, no drift.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

National Insurance (mode 2): the salary sacrifice cap is carried and scoreable — but the default run is not assumption-neutral

1 participant