Repository navigation
Prepare for policyengine-uk's CGT retention-rate default - #633
Conversation
|
@juaristi22 thanks, this is my review at The content before the pin is ready for the pin commit once the body, the fragment and the docs point (4) are fixed. The new case, the default tests and the docs check out, and they pass unskipped on policyengine-uk 2.134.0 and 2.134.3. The pin commit itself needs a look when it lands.
What looks right
What I checked
🤖 Generated with Claude Code |
|
@juaristi22 Round 2 review at There have been no commits or replies since round 1 (16 Oct), so all seven round 1 points are still open: (1) blockers in the body, (2) the list of drift from 2.130.1, (3) the changelog's "unchanged" sentence, (4) the band wording, with PolicyEngine/policyengine-uk#2433 still open, (5) the relief case's tax check, (6) the bare-year reason, and (7) the Blocking
Should-fix
Questions
What I checked: a fresh clone at
I didn't run anything on the real dataset or Hugging Face, the snapshots, Verdict: not ready. Rebase onto contract 2.0.0 and settle the order against the candidate PRs (points 1–3) first. Round 1's points 1–7 are still open. |
Everything policyengine.py needs for policyengine-uk's default capital gains realisation response (policyengine-uk#2396), apart from the pin, which arrives with a qualified candidate pair: - Contract 2.1.0 adds cgt_default_response_relief_gains: 45% higher and additional rates for £50,000 of gains, £20,000 of them qualifying for Business Asset Disposal Relief, whose response is checked at the default elasticity of 1. Like cgt_default_response, its checks wait on policyengine-uk#2396 until the pin moves. The golden rows add 48 rows for it and change none. - tests/test_uk_cgt_default.py checks the default through the population, household and managed routes, an MTR elasticity set alone, and that an unreformed run is unchanged. It skips until the installed policyengine-uk has the default. - The reforms guide gains a section on the response, and a methodology page explains it, with the caveats from policyengine-uk's review: low for rate changes alone, a medium-term value, large rate rises, and household results counting deferred gains as income lost. - The dynamic field's description says None keeps the model's defaults. Part of #602. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The case checked the realised gains and the relief gains' response but not the tax, so it pinned neither the revenue effect for mixed gains nor which gains the annual exempt amount comes off. The new check, pending like the others, expects 45% on the main-rate gains after the exempt amount, which comes off them first, plus the relief rate on the qualifying gains, with both shares scaled by the share-weighted retention ratio. Had the exempt amount come off the relief gains, the change would be £630 more. The golden rows add 10 rows for it and change none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The static-costing example sets both the higher and the additional rate, as the contract cases do. Until policyengine-uk#2433 lands, the model can put gains in the higher band where the law puts them in the additional band, so a reform to one of the two rates can respond where it shouldn't. The methodology page says so. - The bare-year reason says what policyengine-uk does from 2.131.1: fiscal year 2027 for most parameters, calendar year 2027 for growth rates, the indices built from them and household parameters. - Both pages say policyengine-uk applies the elasticity as a power to each person's marginal retention rate, where CenTax apply one aggregate elasticity to the whole base, so adopting their 1.0 reproduces neither their estimates nor their sensitivities. - The docs name policyengine-uk 2.134.0 as the release with the default, which policyengine.py has from the first release that pins it. - The changelog fragment says results don't change until that pin, and becomes an added fragment, since this release adds docs, tests and a contract case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The record test skipped its resolved-assumption asserts whenever the record had none, so on policyengine-uk 2.130.1 they never ran. It now checks that the record has no resolved assumption before 2.134.0, and the retention convention at 1.0 from 2.134.0, so it can't pass vacuously on either side. Also corrects the MIXED_RATIO comment (40% of the gains qualify, not 20%) and the skip guard's comment, now that the pin comes with a candidate pair. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4bd46e2 to
0b83d34
Compare
|
@vahid-ahmadi thanks for both rounds. I've rebased onto main and pushed 0b83d34 in four commits: the rebased one, then one each for the tax check, the docs and the tests. The body is rewritten for the new order. Round 2
Round 1
Checks at 0b83d34
🤖 Generated with Claude Code |
|
@juaristi22 Round 3 at Thanks. All seven round 1 points and all ten round 2 points are closed. I checked each one against the diff. There's one new blocker, and I need to correct something I said in round 2.
What I checked
Verdict: approve once the 2.1.0 full-run fixture is in (point 1). Then merge it soon, and accept no candidate receipt before it's on main. 🤖 Generated with Claude Code |
…ualification gate The gate tests read a real full run of the runner, and fail rather than skip when the contract version moves past it, so contract 2.1.0 failed 69 of them against the 2.0.0 run. This run is contract 2.1.0 on policyengine-uk 2.135.0 with core 3.33.0 and the certified enhanced_frs_2024_25, every route and case, seed 0 and 20 examples: 905 pass, 34 pending policyengine-uk#2396 (both default cases), 120 unsupported, holds. Against the 2.0.0 run it adds the relief-gains case's 40 rows and changes no other row. Its properties and population rows are identical, so it has no count from 1 to 9 and its population details still agree across routes. RELEASED_COUNTS and the full-run test take the new counts, and the fixture README says what was run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@vahid-ahmadi thanks.
🤖 Generated with Claude Code |
|
@juaristi22 Round 4 at Thanks. The round 3 blocker is fixed. The new
Verdict: Approve, once the Test jobs on What I checked
🤖 Generated with Claude Code |
Part of #602.
Merges before the pin, and has none. Since #635, a policyengine-uk release reaches main as a qualified candidate pair. The candidates from 2.134.5 to 2.137.0 (#639–#664) pair each release with uk-data 1.58.2, and every one carries the default. So this PR lands first, and the default goes live when a candidate is promoted. A follow-up then makes the default cases gating. The order is recorded on #602.
PR set: CGT retention-rate default (PolicyEngine/policyengine-uk#2396, #602)
Tracking checklist: checklist on #602
In this PR
All of it is ready for review now. It holds on the current pin (policyengine-uk 2.130.1) and on the newest candidate's (2.137.0).
Contract 2.1.0. It adds
cgt_default_response_relief_gains: 45% higher and additional rates for £50,000 of gains, £20,000 of them qualifying for Business Asset Disposal Relief. It checks the realised gains, the relief gains' own response and the change in CGT, at the default elasticity of 1, against the share-weighted rate. The tax check also fixes which gains the annual exempt amount comes off. Likecgt_default_response, its checks wait onpolicyengine-uk#2396.tests/test_uk_cgt_default.py.mtr_elasticityset alone applies on its own, with oneCapitalGainsResponseWarning, and the record says so.The module skips until the installed policyengine-uk has the default.
The record test in
tests/test_uk_cgt_responses.pychecks both sides of the pin: no resolved assumption before 2.134.0, and the retention convention at 1.0 from 2.134.0.Docs.
The reforms guide gets a section on the response: who is affected, the static settings on each route, the sensitivities, the MTR convention, precedence, setting the same
dynamicon both sides of a comparison, and the assumption's limits.A new methodology page explains how the response is measured and when it is zero. It carries policyengine-uk's caveats:
It also notes that until Stack gains on taxable income when splitting the higher and additional CGT bands policyengine-uk#2433 lands, a reform should change the higher and additional rates together.
The
dynamicfield's description saysNonekeeps the model's defaults.Changelog:
602-cgt-default.added.md. Results don't change until policyengine.py pins policyengine-uk 2.134.0 or later.Why the default cases don't gate yet
A gating default case fails on any model without the default, including main's current pin. policyengine-sim-api's deploy gate also runs the contract's cases on its own model. So they stay pending, reported with whether they hold on every route, until the pin has the default. The candidates' contract gates already run them: #664 reports 19 pending checks, and on 2.137.0 they all hold, as do the relief case's 15.
What the promotion brings (step 5, not this PR)
The default's own effect on unreformed results is nil. On uk-data 1.58.1, none of the aggregates compared moves between 2.133.0 and 2.134.0, for 2025–2027, 2029 or 2030. They cover household net income, taxes, spending and the balance, the main taxes and benefits, and poverty before and after housing costs. The rest of the drift from 2.130.1 is attributed by the candidate's
metrics_reviewandreplaygates.policyengine-uk 2.131.0 to 2.137.0, from its changelog
Changes that move unreformed results:
budget_inputsgate flags these, and Impute additional-dwelling and non-residential purchases, calibrated to HMRC's HRAD statistics policyengine-uk-data#556 tracks them.benefit_capandis_benefit_cap_exempt*.Changes to reforms, branches and interfaces:
labour_supply_responses.obr_elasticitiesis removed.marginal_tax_rate_on_capital_gainsmeasured from the rise in CGT; marginal rates on earnings hold gains fixed.Step 6, after the promotion
test_uk_cgt_default.py, and the pre-2.134.0 branch of the record test.Checks at 1e32c19
test_uk_budget_reform_contract.py,test_uk_cgt_responses.py,test_uk_cgt_default.pyandtest_uk_cgt_response_records.pygive 187 passed. The default module is skipped, as intended.tests/fixtures/uk_contract/full_released_uk_2.135.0.jsonis re-recorded for contract 2.1.0. It covers every route and case, seed 0 and 20 examples, on policyengine-uk 2.135.0 and the certifiedenhanced_frs_2024_25. Result: 905 pass, 34 pending and 120 unsupported. Against the 2.0.0 run it only adds the relief-gains case's 40 rows.test_uk_candidate_gates.pygives 300 passed.ruff format --check .andruff check .are clean.🤖 Generated with Claude Code