Skip to content

Align Child Benefit claim exports and opt-out draws with the UK model - #1107

Open
juaristi22 wants to merge 5 commits into
mainfrom
fix/child-benefit-registered-claims
Open

juaristi22 wants to merge 5 commits into
mainfrom
fix/child-benefit-registered-claims

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

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

  • Full locked-engine UK group before formatting: 164 passed, without fail-fast; locked engine is UK 2.100.0/core 3.32.5.
  • Post-format affected reruns: 32 engine-free, 13 locked-engine and 13 opt-out-aware engine tests passed. The opt-out-aware checkout is UK 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.
  • Synthetic integration before formatting: 2 passed, covering 39 stages and 812 households with zero uploads. Formatting changed only tests/helpers; production code and contract pins remained identical. Test registry, scoped Ruff and whitespace checks passed.

Head: 9ee55c08815231a512b1b1cda601252d0005a260. Remote CI: All 10 checks passed on 9ee55c0 ([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 UK child_benefit formula 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.

  • Pin and record the reviewed engine revision and evaluated opt-out share.
  • Rebuild the Child Benefit stage and downstream data; migrate existing exports explicitly if rebuilding is deferred.
  • Report the eligible-pool shortage and any unmet opt-out target.
  • Measure before/after actual model-paid families, children and total Child Benefit cash on the rebuilt population.
  • Measure OBR fit separately and distinguish this change's effects from broader engine-upgrade changes before releasing data.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort) — round 1 at 0e06c1a7

Verdict: the encoding switch is correct and well guarded. One should-fix: under the new contract the bound Child Benefit amount moves. Plus one note on timing.

Checked and correct:

  • Capability detection. "opt_out_charge_share" in parameters.gov.hmrc.child_benefit.children (child_benefit_take_up.py:268-270). The gov.hmrc.child_benefit node exists on 2.100.0 and on main, so older engines take the legacy path rather than raising. The receipt's claim_export records the encoding, the capability and the engine version string.
  • Exports. Under registered_claims, would_claim_child_benefit = claims on families with an eligible child, and child_benefit_opts_out is exported separately. Under legacy_payment it stays claims and not opted out. Benefit units with no eligible child keep their early draw in both. No flags are OR-ed, so the Enhanced FRS concern from policyengine-uk#2140 round 3 doesn't arise.
  • Draws. Claims and opt-outs use the same identity-keyed draws as before. Only the exported encoding changes.
  • Tests. test_uk_child_benefit_take_up.py (engine-free) passes 16 locally. I didn't run the engine-UK tests (no engine in my environment); the body reports them passing on 2.100.0 and on the #2140 checkout.

1. Should-fix: under registered_claims, the engine pays opted-out families inside the taper, which moves the bound obr.child_benefit amount.

  • With #2140's default opt_out_charge_share = 1, only fully charged opt-outs stay unpaid.
  • The stage fills its opt-out target from fully charged families first, but takes the remainder from inside the taper when there aren't enough. Those taper opt-outs are paid by the new engine.
  • So the baseline Child Benefit amount, which calibration binds through obr.child_benefit (uk_population_targets.json:2155), rises against the legacy encoding. The draw-level in_payment audit no longer measures what the engine pays. The doc says so, but nothing reports the size of the effect.
  • Before the first build on the new contract, either:
    • report the engine-paid families, children and amount beside the draw-level in_payment (one extra receipt line), and check obr.child_benefit still fits; or
    • draw opt-outs only among fully charged families when the engine supports the share parameter, so the draw and the engine agree.

2. Note (timing): the fix is dormant until the build's policyengine-uk pin includes #2140. #2140 is still open and unreleased, and the release builds on a locked 2.100.0, so every build keeps legacy_payment until the pin moves. That's fine and intended (the doc says so). It's worth a line on #1095, so that the engine bump that picks up #2140 also regenerates this stage and checks item 1.

Nit: the docstring's first paragraph now ends "income. This stage" mid-line after the removed clause. Rewrap it.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort) — round 2 at 6a981144

Verdict: not yet. The two new commits only refresh hashes, so the round-1 should-fix is still open. It now matters sooner, because policyengine-uk#2140 is merged and released.

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.

@juaristi22 juaristi22 changed the title Preserve registered Child Benefit claims in UK exports Align Child Benefit claim exports and opt-out draws with the UK model Oct 5, 2026
@juaristi22

juaristi22 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

@vahid-ahmadi Both points in your latest review are addressed in 9ee55c08815231a512b1b1cda601252d0005a260.

The opt-out draw follows your option (a), using the installed opt_out_charge_share: candidates need a positive charge fraction at least that share. With the default share of 1, the draw selects only fully charged families and records an explicit shortfall when that pool is insufficient. Partial shares, the zero-charge boundary, shares above 1 and invalid values are covered. Legacy selection, taper fallback and random streams are preserved. I also rewrapped the module docstring and synchronized the stage declarations, fixtures and dependent hashes.

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: All 10 checks passed on 9ee55c0 ([CI run](https://github.com/PolicyEngine/microcosm/actions/runs/37327907801))..

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 Microsimulation, but the stage's maximum excludes eligible children while child_benefit uses all members. Receipts precede calibration; synthetic tests supply matched income directly and do not establish population fit. No population data was rebuilt or published.

@juaristi22
juaristi22 marked this pull request as ready for review October 5, 2026 15:20

This branch has not been deployed

No deployments
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.

2 participants