Repository navigation
Align Child Benefit claim exports and opt-out draws with the UK model - #1107
Conversation
Automated review pass (Claude Code, high effort) — round 1 at
|
Automated review pass (Claude Code, high effort) — round 2 at
|
| Item | Status | Evidence |
|---|---|---|
| Should-fix: taper opt-outs are paid under the new encoding | Open | assign_child_benefit_opt_outs (child_benefit_take_up.py:667-700) still fills the shortfall from the taper pool, whatever supports_opt_out says. Under registered_claims those families keep would_claim_child_benefit. With policyengine-uk's default opt_out_charge_share of 1, the engine pays them, because only fully charged families stay opted out. So once the encoding switches, the paid caseload rises by the taper pool's opted-out mass, and so does the amount obr.child_benefit calibrates against, while the receipt's "paid" count (claims & ~opt_out, :452) understates what the engine pays. Fix, either: draw only from fully_charged when supports_opt_out is true (recording the shortfall); or report the engine-paid caseload next to the stage count in the receipt and confirm the Child Benefit rows still fit. |
| Nit: docstring wrap | Open | The module docstring still breaks at "income. This stage" (:6). |
| New: refreshed hashes | Fine | 4d03a7a4 updates the coverage manifest's source_manifest_sha256 entries. 6a981144 updates the synthetic UK graph's Child Benefit contract hash in the parity fixture. Both follow from the stage change. |
The capability check still matches policyengine-uk. #2140 merged on 5 October (fdad84ca) and ships in 2.121.0. policyengine_uk/parameters/gov/hmrc/child_benefit/opt_out_charge_share.yaml is the name this PR tests for ("opt_out_charge_share" in parameters.gov.hmrc.child_benefit.children, :268-269), and child_benefit again uses defined_for = "would_claim_child_benefit". That is the semantics registered_claims assumes.
Still dormant. uv.lock pins policyengine-uk 2.100.0, so releases keep the legacy encoding until the build moves to ≥ 2.121.0. That move would bring about twenty releases of benefit changes with it, so it belongs on #1095 with its own measurement arm. The taper fix above should land before it, so that the switch doesn't move the Child Benefit fit unannounced.
Ran locally at this head: test_uk_child_benefit_take_up.py and test_uk_release_input_coverage_manifest.py (31 passed). CI: integration-uk, lint, select-countries and wheels are green; engine-free and engine-uk/us were still running.
|
@vahid-ahmadi Both points in your latest review are addressed in The opt-out draw follows your option (a), using the installed Local validation passed the full UK group (164 tests) and synthetic integration (2 tests). After formatting tests/helpers, affected reruns passed 32 engine-free, 13 locked-engine and 13 opt-out-aware engine tests; production code and contract pins were unchanged. The synthetic dataset cases check actual model-paid families, children and cash at shares 1, 0.5, 0 and 1.1. Remote CI: The registered-claim export complements the gate merged in UK#2140. I added the rollout checklist to this PR, linked to #1095: pin the engine, rebuild or explicitly migrate exports, report shortages, and measure population model-paid counts/cash and OBR fit separately from broader upgrade effects. Production adjusted net income comes from UK |
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Automated review pass (Claude Code, high effort) — round 3 (final) at 9ee55c08
Verdict: approve. The round-2 should-fix and the nit are both closed, CI is 10/10 green, and the one new point below is a note for the engine, not a change here.
| Item | Status | Evidence |
|---|---|---|
| Opt-outs drawn from the taper under the new encoding | Closed | Under an opt-out-aware model, candidates now need children, a positive charge fraction, and a fraction of at least the installed opt_out_charge_share (child_benefit_take_up.py:686-700), which mirrors the engine's suppression rule. With the default share of 1 only fully charged families qualify, the taper pool is empty, and any gap is recorded as pool_shortfall_families / pool_exhausted. The receipt also records the share used. |
| Edge cases | Covered | test_new_opt_out_pool_matches_positive_charge_share_boundaries pins shares 1, 0.5, 0 and 1.1, including the charge-start boundary (share 0 still pays a family exactly at £60,000). test_opt_out_capability_requires_a_finite_numeric_share refuses None, NaN, inf, strings and booleans. test_new_contract_reports_shortfall_without_paid_taper_spillover checks that the shortfall is reported instead of spilling into paid taper families. |
| Legacy path unchanged | Confirmed | The new filter sits behind thresholds.supports_opt_out. The pool loop, rates and draws are as before, so the locked 2.100.0 build draws exactly as at 6a981144. The fixture, source-stage and spec changes are the rule and notes text only. |
| Docstring | Closed | The broken line at the top is rejoined, and the opt-out paragraph now describes both contracts. |
| Rollout checklist | Present | It's in the PR body, linked to #1095: pin the engine, rebuild or migrate the exports, report shortfalls, and measure paid counts and cash against the OBR row separately from the wider upgrade. Worth copying a one-line item onto #1095 itself so it's tracked there after merge. |
Locally, the engine-free test_uk_child_benefit_take_up.py passes (32). This venv has no engine, so the locked-engine and opt-out-aware engine tests come from CI's engine-uk job, which is green.
On the income-maximum mismatch you flagged. The stage takes the highest adjusted net income over members who aren't eligible children (:431-435). policyengine-uk's child_benefit and CB_HITC take it over all benefit-unit members, so a qualifying young person's income counts there. The stage is the one that follows the law: the charge falls on the claimant or partner with the higher adjusted net income (ITEPA 2003 s.681B–681D), and a child's income never triggers it. The mismatch only bites for a qualifying young person in non-advanced education whose own adjusted net income is above £60,000, in which case the stage doesn't treat the family as charged, so it never opts out, while the engine charges it on the young person's income. That's vanishingly rare in the FRS, so it doesn't need a change here. A note in the docs, plus a policyengine-uk follow-up to read is_claimant_or_partner in both variables, would close it properly.
The commits, where the plan changed during implementation (C5 reads the child tab too; C6 puts one identity-keyed draw on the root stage; B1 mirrors the engine's formula rather than handing the flag to it; B2 leaves the UC take-up population to Max's branch; B4 is rebased on #1081, which merged first), and the measurement: arm E (value-identical LCFS, 7/7 gates), arm B0 (the engine bump empties two sparse UC payment bands; #1107's rollout checks), arm B2 and the head arm, which also takes the West Midlands 12,570 to 15,000 income-tax cell past the 25% bound. Both diagnoses are recorded: the regional bands leave out other investment income, and policyengine-uk 2.122.2's corrections move every award that supported the two UC bands. X defers the cell and Y excludes the bands; replaying the target-fit gate on the head arm's evidence then passes. Review round 1 on #1121: the rebase onto #1081, the battery clock CI tripped on, and R1's access-fund evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Child Benefit payment flags previously erased registered opt-outs from the claim population, preventing charge-relief reforms from restoring their payments. This PR preserves registered claims when the installed engine supports opt-outs, complementing the independent claims gate merged in PolicyEngine/policyengine-uk#2140 for #2065. Older engines retain the existing payment-flag export.
It also addresses Vahid's latest review: the new contract's taper fallback could select families that the model still paid. The stage now reads and records the installed
opt_out_charge_share, requiring a positive charge fraction at least that share. At the default share of 1, only fully charged families qualify; an insufficient pool produces an explicit weighted shortfall. Partial shares, zero-charge boundaries, shares above 1 and invalid values have regression coverage. Legacy pool selection, taper fallback and random streams remain unchanged. The module-docstring wrap is fixed, and canonical declarations, synthetic fixtures and dependent hashes are synchronized.Validation
fdad84ca3fd2a81774211e888dc81565a64d4b22/core 3.32.16; tests check synthetic exported-dataset paid families, children and cash at shares 1, 0.5, 0 and 1.1, plus charge-relief restoration.Head:
9ee55c08815231a512b1b1cda601252d0005a260. Remote CI:All 10 checks passed on9ee55c0([CI run](https://github.com/PolicyEngine/microcosm/actions/runs/37327907801))..These synthetic checks supply matched adjusted net income directly. The production adapter calculates adjusted net income through a UK
Microsimulation; the stage then takes its maximum over members who are not eligible children, while the UKchild_benefitformula uses all members. Receipts audit draws before calibration. Population cash caseload and calibration fit therefore require measurement. This PR does not rebuild or publish population data or change dependency locks. Existing folded exports require a rebuild or an explicit migration before using the registered-claim contract.Rollout checklist
Related follow-up: #1095.