DO NOT MERGE YET: Add an initial council tax reduction framework#1534
DO NOT MERGE YET: Add an initial council tax reduction framework#1534MaxGhenis wants to merge 105 commits into
Conversation
|
This draft still looks like real work, but not something to merge in its current form. It is stale, conflicting with current main, and large enough that the right next step is probably to split it into smaller PRs rather than revive this branch directly. A sensible breakdown would be: 1. core CTR variable/plumbing and netting behavior, 2. pensioner/Wales/Scotland baseline support, 3. local-authority-specific overrides and comparison scripts. If no one is planning to actively do that split soon, closing this draft would be cleaner than leaving it to rot. |
# Conflicts: # policyengine_uk/tests/microsimulation/reforms_config.yaml
|
Checkpoint recap after Coventry batch (
Local verification before push: uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml -n Coventry # 14 passed
uv run ruff check policyengine_uk/variables/gov/local_authorities/coventry/council_tax_reduction/*.py policyengine_uk/variables/gov/local_authorities/council_tax_reduction/config.py policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py
uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml -c policyengine_uk # 672 passed
uv run python - <<'PY'
from policyengine_uk import CountryTaxBenefitSystem
system = CountryTaxBenefitSystem()
print("import smoke ok", len(system.variables))
PY
git diff --checkFresh GitHub CI is pending on the pushed commit; PR mergeability is currently Next queued source candidate in the handoff is Cotswold. |
|
Checkpoint: added Cotswold CTR at bbedb5e. Modeled from the official Cotswold 2026/27 scheme PDF:
Verification before push:
I also trialed the cheaper-agent workflow: a 5.4-mini scout produced a usable Cheltenham dossier, but flagged the non-dependant table as OCR-sensitive. That seems like the right split: mini/5.4 for scout and draft implementation on simple schemes, 5.5 for source-fidelity review and integration. |
|
Checkpoint: added Cheltenham CTR scheme in Modeled from Cheltenham Borough Council's adopted 2026/27 Appendix 9 source: household-type weekly net-income bands, Band E cap, GBP 6,000 capital limit, no tariff income, GBP 10 weekly earnings disregard, disabled-child disregard, UC assessed-income/capital branch, pension-age UC/income-based benefit local routing, childcare excess against tax credits, and gross-income non-dependant deductions before the percentage. Source-review note: a subagent review initially found UC pre-disregard earnings, pension-age UC routing, childcare/tax-credit ordering, and non-dependant ordering test gaps. Those are fixed and the reviewer re-check came back clean. Local verification:
Next queued source dossier: Bassetlaw 2026/27, banded working-age scheme with Band C cap, 95/88/65/45/25/0 support bands, GBP 16,000 capital limit, UC award/income handling, and non-dependant deductions. |
|
Bassetlaw checkpoint pushed in 8fe3f47. Added Bassetlaw's 2026/27 working-age CTR scheme from the council PDFs, with:
Verification:
policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml ..................... ============================== 21 passed in 9.70s ============================== -> 21 passed
Coverage doc/work queue now says 82 current English working-age billing authorities, plus Wales/Scotland national schemes. |
|
South Derbyshire checkpoint pushed in Added South Derbyshire's 2026/27 working-age CTR scheme from the official council PDF/current page:
Subagent review found and I fixed:
Local verification:
Coverage note updated to 87 current English working-age authorities. |
|
Checkpoint after Bath and North East Somerset CTR ( What changed:
Verification:
Minimal Claude Code prompt to continue: |
# Conflicts: # uv.lock
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Review
Branch: codex/ctr-framework · 102 commits · 1,543 files · +76,022/−9
Verdict
Do not merge as-is. The architectural direction is sound and the local-authority work is generally high-quality, but the PR cannot be responsibly reviewed at this size. The split into 3 PRs proposed earlier in this thread is still the right call. The unblockable concern below makes a single-PR merge risky regardless of size.
Architecture (good)
Layering is clean and the contract is clear:
council_tax_less_benefit (household)
└─ council_tax − council_tax_reduction ← clamped at 0
council_tax_reduction (household)
└─ Σ council_tax_benefit per benunit head
council_tax_benefit (benunit)
└─ where(supported, simulated_council_tax_reduction_benunit, reported)
simulated_council_tax_reduction_benunit (benunit)
├─ national: England-pensioner / Wales / Scotland
└─ local: Σ of ~95 per-LA award variables
- Shared helpers in
_legacy.py(legacy_council_tax_reduction,local_non_dep_deductions,normal_gross_income_non_dep_deduction) cut duplication well. - Per-LA folder structure is consistent:
<la>_council_tax_reduction.py,*_individual_non_dep_deduction.py,*_non_dep_deductions.py, optionally*_is_local_scheme.py/*_uc_applicable_income.py/*_maximum_eligible_liability.py. - Parameter trees are well-organised and (spot-checked) every YAML has
metadata.referencewith title + href to a primary council source.
Issues to address
1. Unblockable: hbai_household_net_income baseline shift
subtracts changed from council_tax → council_tax_less_benefit (hbai_household_net_income.py:62). This changes baseline net income for every UK household, not only the ~95 modelled LAs — for unmodelled LAs, council_tax_reduction becomes the reported-CTB sum, which previously wasn't being netted out at all. That's likely the correct HBAI methodology, but it's a silent baseline shift that needs an explicit micro-impact sanity check (CI poverty/inequality stats) before going to main. Not a CTR-framework concern per se; it'll move everything.
2. simulated_council_tax_reduction_benunit.py is brittle
The "exclude LAs that have their own pensioner scheme from the national-pensioner branch" is a hand-maintained & ~X_local_scheme chain (~40 entries). Adding a new LA in three places (LOCAL_COUNCIL_TAX_REDUCTION_VARIABLES, the fetch block, the exclusion AND) and forgetting one silently double-counts (national + local award). No assertion or schema enforces consistency. Worth refactoring to a registry/declarative table before more LAs are added.
3. locations.py enum: look-alike duplicates
Added BRISTOL, HEREFORDSHIRE, KINGSTON_UPON_HULL, DURHAM alongside existing BRISTOL_CITY_OF, HEREFORDSHIRE_COUNTY_OF, KINGSTON_UPON_HULL_CITY_OF. Also added CUMBERLAND, NORTH_NORTHAMPTONSHIRE, NORTH_YORKSHIRE, SOMERSET, WEST_NORTHAMPTONSHIRE, WESTMORLAND_AND_FURNESS (the 2023 unitary-restructure LAs — those are legitimate). The first group needs justification in the PR description; if these are ONS-naming variants the dataset emits, fine — but if a household could land in either bucket, CTR matching will silently miss.
4. programs.yaml entry is stale and missing fields
The new council_tax_reduction entry in policyengine_uk/programs.yaml says "explicit overrides for Stroud and Dudley" — actual coverage is ~95 LAs. Also missing parameter_prefix (required per CLAUDE.md). verified_years: \"2025-2025\" is suspect since most parameters key on 2026-04-01.
5. Test coverage is bimodal
960 YAML tests is healthy in aggregate, but ~18 LAs have ≤2 tests covering capital limits, tariff income, protected-group exemptions, and UC routing — each of which is a multi-branch formula. Examples: Bury/Warrington/Stockport (band-cap, 2–3 tests each), West Berkshire (custom capital boundary, 2 tests), Tendring (JSA-3-year flag, 2 tests), Basildon's is_local_scheme (multiple UC branches, sparse coverage). Recommend ≥5 tests per LA with non-trivial branching before merge.
6. Naming will trap future contributors
council_tax_benefit now means CTR. council_tax_reduction is just the household sum. The wrapping is functional but the naming inverts intuition. Consider renaming as part of the split — follow-up, not a blocker.
7. Comparison scripts (scripts/{entitledto,policyengine,turn2us}_ctr_compare.py, 1,344 lines combined)
Useful dev harnesses, but pinned into the main repo without README or CI hookup. Either document them in CONTRIBUTING/README or move to scripts/dev/ with a one-line purpose comment in each file.
8. Stray dev artefacts shipped in the package tree
scheme_work_queue.md, agent_handoff.md, scheme_encoding_guidance.md live inside policyengine_uk/variables/.... They're useful but should be in docs/ or top-level — they currently ship in the installable package.
Spot-check positives
- Basildon main formula correctly gates on
local_scheme & benunit_contains_household_head & would_claim & capital_eligible— pattern matches across sampled LAs (Coventry, Chichester, Bury, Tendring). - Parameter YAMLs cite specific S13A PDFs / Cabinet papers, not generic council pages.
legacy_council_tax_reductioncorrectly suppresses excess-income for working-age claimants on income-based benefits (excess_income = where(working_age & relevant_income_based_benefit, 0, excess_income)).would_claim_council_tax_reductioncorrectly OR's the takeup behaviour flag withreported > 0so existing claimants don't disappear from baseline.
Recommended path forward
Endorsing the earlier proposal — split into:
- Core CTR plumbing: shared framework files,
council_tax_benefitrouting change,council_tax_less_benefitcleanup,hbai_household_net_incomeshift — with explicit baseline-stats validation. - National schemes: England-pensioner / Wales / Scotland.
- Per-LA overrides: in waves, with the
simulated_council_tax_reduction_benunitregistry refactor first so each subsequent LA PR touches one place, not three.
If it has to land as one piece, the unblockable items are #1 (baseline-impact check), #3 (enum duplicates) and #4 (programs.yaml).
# Conflicts: # policyengine_uk/variables/household/income/hbai_household_net_income.py
Summary
council_tax_benefit,council_tax_less_benefit, and household net income73current English working-age billing-authority schemesCurrent CTR Scheme Coverage
73current English working-age billing authorities are implemented in this PRcouncil_tax_benefitvalues rather than guessed local rulesSource And Modeling Notes
scheme_work_queue.md; for example Plymouth's adopted PDF controls over a conflicting live summary amountValidation
uv run policyengine-core test policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml577 passed in 177.17suv run ruff checkon touched CTR variable/shared filesuv run ruff format --checkon touched CTR variable/shared filesgit diff --checkLatest Checkpoint
Paused Next Work