UK spine: benefit-unit-grain SPI UC reporter draw with pre-take-up screen (#832); exact capital sentinel (#833) - #835
Conversation
…d blocker) The first licensed spine-m build failed at uc_reporter_redraw: 'Every benefit unit must contain an adult claimant candidate.' A 16-19 qualifying young person heading their own benefit unit is its only member, so the engine's UC child flag leaves no ~uc_child candidate — the licensed frame carries 238 all-under-18 benefit units, and PR CI can never reach this path (secrets-free, no licensed builds). The fallback now takes the unit's eldest member as its de-facto head, and the fail-closed check moves to the landing, where a POSITIVE draw on a child claimant refuses — the award screen keeps child-only units out of the drawn domain, so a positive landing there is exactly the corruption the old total guard feared. Regression tests for both halves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Automated review pass (Claude Code, high effort, diff plus branch reads — no execution). Draft, so nits skipped. The #833 sentinel fix verifies cleanI filed that issue so I checked it rather than assuming. All four Four findings on the #832 stage. 1.
|
Licensed I5/I6 measurement: spine-m + seam v17 (2026-09-01, measurement session)Ran this PR's I5 (twin rebuild) and the I6 recalibration on María's direction. Comment-only interaction as before — all runs live on Two licensed-build blockers found and fixed on the measurement stack (cherry-pickable)PR CI is secrets-free, so neither could surface before a licensed build:
I5 twin receipt — attribution is exactThird build green (14/14 gates, engine 2.92.1, frame shape unchanged). Column-level twin compare vs spine-l: the entire diff is four columns, all owned by the two UC stages — I6 verdict — the honest answer to "does this fix the with-children gap": no. It fixes something else that needed fixing.v16 (spine-l) → v17 (spine-m), initial = pre-solve support, fix-isolated:
Conclusion: this PR makes the reporter channel truthful — a prerequisite — but the with-children caseload deficit survives it intact, and is now even more precisely located: the screened-eligible SPI domain (2.416m weighted) simply does not contain enough with-children claimants at faithful base rates. The B-lane ( Housekeeping the run settled
Measurement session (Claude Fable 5, at María's direction). |
…d blocker) The first licensed spine-m build failed at uc_reporter_redraw: 'Every benefit unit must contain an adult claimant candidate.' A 16-19 qualifying young person heading their own benefit unit is its only member, so the engine's UC child flag leaves no ~uc_child candidate — the licensed frame carries 238 all-under-18 benefit units, and PR CI can never reach this path (secrets-free, no licensed builds). The fallback now takes the unit's eldest member as its de-facto head, and the fail-closed check moves to the landing, where a POSITIVE draw on a child claimant refuses — the award screen keeps child-only units out of the drawn domain, so a positive landing there is exactly the corruption the old total guard feared. Regression tests for both halves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Folded the two licensed-build fixes from the spine-m measurement into this PR as their own commits (cherry-picked from |
Addendum: the full-surface measurement, and a merge assessment (all 364 bound targets, v16 → v18)The composition table above under-reports what this PR does. Measured across the whole bound surface, fix-isolated (initial layer, pre-solve):
What got worse — the complete accounting. 115 of 193 final-layer movers slipped, none by more than ~1.4pp ( Merge assessment (measurement session's recommendation to María): merge-worthy, on two conditions. For: (1) semantic correctness that survives outside calibration — reporter→award conversion ≈100% by construction, so dashboards, reform deltas, and take-up analysis stop reading 1.24m impossible claimants; (2) the payment-distribution cleanup is the largest structured UC improvement since the #755 pension split, and it is pre-calibration shape, not solver make-up; (3) it is the foundation the actual gap-closer builds on — the B-lane must draw with-children reporters into an honest channel; (4) the effect is perfectly contained and receipted. Conditions: (1) fold in the two licensed-build fixes ( Measurement session (Claude Fable 5); posted at María's direction. |
|
Second pass at Status of the first-pass findingsNone of the four is touched by 1. Reframed, still open — this is a spec-truthfulness gap, not a destructive bug. Reading the I5 measurement comment made it clear that zeroing screen-failing SPI reporters is the fix — that is the "dead mass demoted" line in the twin receipt. So the write at 2. Still open, and I would still hold merge on it. 3. Still open. 4. Still open. The two folded commitsThe claimant fallback and the adult-member screen are the right fixes: the old total guard genuinely could not survive a qualifying-young-person-only unit, and 5. The receipt files child-only units under 6. The relocated landing guard is unreachable in the pipeline — same shape as finding 3. 7. No end-to-end test drives a child-only SPI benunit through the stage. The two new tests are real (the claimant one would have raised before Checked and clear
2 is the merge-blocker as before. 1 is now a spec-truthfulness fix rather than a behaviour change, and 5 matters because it corrupts the very cell the measurement session is reading. |
|
Disposition of the second pass (at 1. Spec truthfulness — fixed at the spec, not the code. The declared screen is now 2. Empty screened SPI domain — refuses. 3. The tautological retention check — replaced by invariants the write did not derive. 4. The 5. Child-only units misfiled as 6. The landing guard's framing — corrected. Comment now says what it is: defence in depth against hand-fed draws and a future screen regression, unreachable through the stage's own screen. The commit message that called it a pipeline fence stands as history. 7. End-to-end child-only test — added. Minors, both taken. One item that is an adjudication, not a fix. The superseded Verification: UK+machinery funnel, |
…d blocker) The first licensed spine-m build failed at uc_reporter_redraw: 'Every benefit unit must contain an adult claimant candidate.' A 16-19 qualifying young person heading their own benefit unit is its only member, so the engine's UC child flag leaves no ~uc_child candidate — the licensed frame carries 238 all-under-18 benefit units, and PR CI can never reach this path (secrets-free, no licensed builds). The fallback now takes the unit's eldest member as its de-facto head, and the fail-closed check moves to the landing, where a POSITIVE draw on a child claimant refuses — the award screen keeps child-only units out of the drawn domain, so a positive landing there is exactly the corruption the old total guard feared. Regression tests for both halves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
7a49e2d to
f2271ee
Compare
…d blocker) The first licensed spine-m build failed at uc_reporter_redraw: 'Every benefit unit must contain an adult claimant candidate.' A 16-19 qualifying young person heading their own benefit unit is its only member, so the engine's UC child flag leaves no ~uc_child candidate — the licensed frame carries 238 all-under-18 benefit units, and PR CI can never reach this path (secrets-free, no licensed builds). The fallback now takes the unit's eldest member as its de-facto head, and the fail-closed check moves to the landing, where a POSITIVE draw on a child claimant refuses — the award screen keeps child-only units out of the drawn domain, so a positive landing there is exactly the corruption the old total guard feared. Regression tests for both halves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
f2271ee to
cf16f45
Compare
|
Third pass at Dispositions1. Spec truthfulness — verified. 2. Empty screened domain — verified. 3. Landing invariants — two of three tested to failure. 4. Incidence signature — disposition right, one sentence of the evidence wrong. Keeping 5. 6. Guard comment — verified ( 7. End-to-end child-only test — verified. It drives a 17-year-old QYP-only SPI unit carrying a chain fill of 300 through the stage with an engine that pays it, and asserts exit at 0, 8. Minors — verified for wiring; one caveat. 9. The fixture re-pin (
|
I2-I4 of the approved plan: new uc_reporter_redraw stage at index 19 (temp-frame engine screen on positive pre-take-up award with FRS-carried capital; benefit-unit RegimeGatedQRF seed 44 trained on the screened base domain; single-claimant landing), the derived-surface sweep (schema branch, op-kind allowlist, sources lockstep, roster/sha/binder pins, coverage manifest), and the two bounded signed differences with I1 receipt anchors (experiments/832-uc-reporter-receipts.md). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Co-Authored-By: Codex <noreply@openai.com>
np.isclose's default band admitted ~[-1.00001, -0.99999] as the sentinel, silently reading near-sentinel corruption as a declared absence. Exact equality in the stage-entry check, the gate-binding domain predicates, and the sentinel-parity check; near-sentinel regression tests at both layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d blocker) The first licensed spine-m build failed at uc_reporter_redraw: 'Every benefit unit must contain an adult claimant candidate.' A 16-19 qualifying young person heading their own benefit unit is its only member, so the engine's UC child flag leaves no ~uc_child candidate — the licensed frame carries 238 all-under-18 benefit units, and PR CI can never reach this path (secrets-free, no licensed builds). The fallback now takes the unit's eldest member as its de-facto head, and the fail-closed check moves to the landing, where a POSITIVE draw on a child claimant refuses — the award screen keeps child-only units out of the drawn domain, so a positive landing there is exactly the corruption the old total guard feared. Regression tests for both halves. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The relocated landing guard caught the second half of the child-only defect on the retry build: the engine computes a positive pre-take-up award for a benefit unit whose only member is a 16-19 qualifying young person, though such a unit cannot claim — so the award screen alone admits child-only units into the drawn domain and the model lands positive draws on child claimants. The screen now also requires a non-child member; the landing guard stays as the fail-closed fence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Spec truthfulness: the declared screen is claimable_screen (positive pre-take-up award AND a non-child member) and the rewrite domain is its own field (rewrite_rows: spi_channel) beside the draw domain; schema, drift fence and stage notes follow. An empty screened SPI domain now refuses instead of zeroing the channel. The tautological retention check is replaced by landing invariants the write did not derive (base column byte-identical, per-unit sums equal the draws, one positive row per reporting unit). The transition receipt files child-only units under child_only rather than single_with_children. The landing guard is framed as defence in depth. An end-to-end QYP-only SPI unit test covers the licensed-build failure path. Working age comes from the engine's is_SP_age; _has_adult_member refuses person-less units. The universal_credit_reported signature is re-derived on the spine-m actuals (Part F); the would_claim_uc entry records its measured in-band actual pending adjudication. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The uc_reporter_redraw stage is declared in the UK spine graph (open input surface, one rewritten person cell) and wired into the kernel registry and the fixture tool; the hermetic fixture's descriptor and pinned graph move with it. Regenerated with tools/graph_uk_spine_fixture.py at this head. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ass (#832) María's ruling on the spine-m actuals: would_claim_uc lands 0.001951 below the incumbent, inside the whole-spine acceptance band, so the #828-era candidate_above signature (bound 0.0553) is retired rather than re-signed; the register test now asserts its absence and Part F records the ruling. The incidence entry's evidence states the real crossing point (about 2,091 of the 3,299 screened SPI units, ~63%, against the measured 39.5%) instead of the range ceiling. The sum-equals-draw landing invariant gains its own failing-path test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
cf16f45 to
7f3a9cd
Compare
The licensed acceptance was re-run on the rebased tree: twins e9r-twin-a and e9r-twin-b (28 stages) are payload-identical and pass the spine battery, the control is rebuilt from main 47c7422, attribution again isolates exactly the six E9 columns with E5 byte-equal, the e9 identity receipt passes, and the six columns are byte-identical to the pre-rebase twins. The engine round-trip figures move slightly with the reporter redraw upstream (mean monthly 66.81 / 50.66; at-cap 0.145 / 0.263; unique-row incidence z 1.84 / 2.00). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h age_tail (#785, #845) The graph's CREATE node declared person.age as float64 while the legacy frs_spine root produces int64, so every graph stage saw a float age from node 0 and the legacy plan an integer until age_tail rewrote it. The interface forbids a rewrite from changing its base's dtype, so the honest declaration is int64 end to end: the root cell is declared int64, age_tail's rewrite is declared int64, and the transform preserves an integer input dtype (its bands and within-band offsets are integral). frs_education's explicit int64 cast stays as the stage's own contract. The exported age column changes dtype (float64 to int64) and nothing else: the payload expectation declares that as a dtype-only change through a new per-group surfaces allowlist in the classifier, so a value difference on age stays unexpected. The H2 fixture is re-pinned; the receipts checklist names the graph-executed post-#835 build as the before artifact, since the legacy-executed spine-m carries executor normalizations that are not this change's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…h age_tail (#785, #845) The graph's CREATE node declared person.age as float64 while the legacy frs_spine root produces int64, so every graph stage saw a float age from node 0 and the legacy plan an integer until age_tail rewrote it. The interface forbids a rewrite from changing its base's dtype, so the honest declaration is int64 end to end: the root cell is declared int64, age_tail's rewrite is declared int64, and the transform preserves an integer input dtype (its bands and within-band offsets are integral). frs_education's explicit int64 cast stays as the stage's own contract. The exported age column changes dtype (float64 to int64) and nothing else: the payload expectation declares that as a dtype-only change through a new per-group surfaces allowlist in the classifier, so a value difference on age stays unexpected. The H2 fixture is re-pinned; the receipts checklist names the graph-executed post-#835 build as the before artifact, since the legacy-executed spine-m carries executor normalizations that are not this change's. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Closes #832, with #840 as the next step, plus the #833 sentinel fence fix folded in on request. Base: main (post-#844). Seven commits: the stage with its graph declaration (#832), the sentinel fix (#833), the two licensed-build child-only fixes, the second-review disposition, the H2 parity fixture re-pin, and the third-review close-out (the
would_claim_uclift signature retired on the spine-m actuals, María's ruling).What this does (#832, fix 1)
New stage
uc_reporter_redrawat driver index 19, betweenhmrc_spi_income_spineanduc_capital_coherence: rewritesuniversal_credit_reportedon SPI-channel rows at benefit-unit grain, replacing the person-grain chain fill that produced ~1.24m weighted dead reporters (uc_income_reductionexhausts the award).universal_credit_reportedis chain position 6 of 29 — removing it would redraw 23 downstream columns and move the pinned output surface). Stage-18 outputs stay byte-identical, so the I5 twin compare attributes every diff to this fix.max(0, uc_maximum_amount − uc_income_reduction) > 0withuc_reported_capital := frs_benunit_capitalderived on the temp frame only (nothing engine-known persisted; the coherence stage keeps ownership of the persisted column). Hard restriction: only screened-eligible benefit units can be drawn reporters; the model is trained on the screened base domain so no rate renormalization exists. SPI reporter→positive-award conversion becomes ≈100% by construction.RegimeGatedQRF(seed=44)via its existing DataFrame front door (zero microcosm-fit changes) — the zero/positive regime gate is the income- and composition-conditioned receipt draw; the positive forest draws the amount. Predictors: is_married, UC child band (0/1/2/3+ from the engine's qualifying-young-person flag), BU employment/self-employment/investment income, claimant + partner earnings split, claimant age, region.person_idtie-break) — matches the base channel's single-payee pattern (frs_spine.pymaps each FRS benefits record to its holder).uc_capital_coherencepicks up the new reporter set automatically (anchor → OR-refresh → capital redraw).I1 receipts (licensed, on spine-l
5176e6ec…; committed evidence:experiments/832-uc-reporter-receipts.md)#833 (second commit)
Exact sentinel comparison at both domain layers (stage entry + gate binding, including sentinel parity):
np.isclose's band admitted ~[−1.00001, −0.99999] as "the sentinel", silently reading near-sentinel corruption as declared absence. Near-sentinel regression tests added at both layers.Signed differences
Two entries, both I1-derived bounds — I5 re-derives the actuals:
uc-reporter-claim-refresh-lift(the UK spine: preserve FRS benefit-unit capital for UC while retaining WAS household wealth #828 lift signature) retired: spine-m measureswould_claim_ucat 0.548741, 0.001951 below the incumbent and inside the ±0.02 acceptance band, because the redraw's demotions outweigh the OR-refresh lift — an in-band difference carries no signature, so a future out-of-band move in either direction is flagged as unsigned (Part F of the receipts doc records the ruling).uc-reporter-benefit-unit-redraw-incidence: personuniversal_credit_reportednonzero share, incumbent 0.057359,candidate_below,max_abs_delta0.0184 (structural range [0.038962, 0.067990]; model-expected ≈ 0.050).Derived surfaces
Schema
oneOfbranch + op-kind allowlist forredraw_spi_reported_uc; sources.yaml↔source_stages.json lockstep; roster 28→29 stages; UK spec sha re-pin; coverage-manifest sha regeneration; acceptance-receipt binder generalized to the two pending stages (I5 restores strict roster equality when it re-mints the receipt — carried from #829). Not moved, by design:uk/gates.json, contract digests, export allowlist, US surfaces.Verification
Full UK+machinery funnel green locally (
test_uk_*+ country_spec + spec_engine + source_runtime + stochastic_assignment + gate_battery + gates + release_input_coverage + data contract), plusruffandtools/ci_test_groups.py --verify. Two documented pre-existing licensed-cache regeneration failures (engine 2.89.0 committed evidence vs 2.92.1 floor, PR #749's lane) — their inputs are untouched by this diff.Out of scope / next
num_uc_childrenengine variable + floor bump + numbered-child rebind): specced in plan D5, handed off on UK spine: composition-faithful SPI income draw — bridge predictors and rank-preserving replacement (with-children UC residual) #840.Closes #833 and #832. The with-children residual continues as #840 (composition-faithful SPI income draw), which carries the B-lane (
num_uc_children) hand-off in its thread; the I5/I6 measurements for this PR are recorded in the comments.🤖 Generated with Claude Code