Skip to content

UK spine: preserve FRS benefit-unit capital for UC, re-anchor take-up after SPI (#828) - #829

Merged
juaristi22 merged 3 commits into
mainfrom
uk-uc-capital-coherence-828
Sep 1, 2026
Merged

UK spine: preserve FRS benefit-unit capital for UC, re-anchor take-up after SPI (#828)#829
juaristi22 merged 3 commits into
mainfrom
uk-uc-capital-coherence-828

Conversation

@juaristi22

@juaristi22 juaristi22 commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Implements #828 increments I2–I6 from the reviewed plan (uk-828-uc-capital-plan.md, both adjudication rounds folded in). Codex-implemented under the approved-plan protocol; reviewed increment-by-increment against the plan before this PR. Closes #828.

What this does

  • I2 — carrier: frs_spine carries benunit.TOTCAPB4 as engine-unknown frs_benunit_capital behind the named UC_CAPITAL_UNAVAILABLE = -1.0 sentinel. The I1 vintage audit found the column fully populated (18,850/18,850 rows ≥ 0), so zero rows map to the sentinel this build; the mapped-row count is reported in stage checkpoint evidence so a future vintage with absences announces itself.
  • I3 — uc_capital_coherence stage (between hmrc_spi_income_spine and cgt_incidence_clone, i.e. after the last universal_credit_reported writer): redraws capital only for SPI-channel post-fill UC reporters (weighted empirical draw from base-FRS reporter capital within dependent-children band × couple cells, identity-keyed seed, fail-closed on empty donor cells), then applies the monotone would_claim_uc |= reported refresh and materializes uc_reported_capital for the policyengine-uk 2.92.1 means-test seam. Mid-build engine materializations are untouched by design, so the WAS/LCFS/ETB surfaces stay bit-identical.
  • I5 — gates: new country-agnostic column_implication gate primitive (shared layer) with the uk_uc_capital_coherence terminal release-blocking binding composing implication + sentinel floor + sentinel parity + same-source equality. Stage-order regression test + requires_uk engine same-source test (reporters with FRS capital ≤ £16k get uc_assessable_capital == uc_reported_capital; −1 still routes to the residual proxy).
  • I4/I6 — derived surfaces + register: sources.yaml edited as authority with the projection regenerated; schema branch for the new op kind; coverage manifest re-cut; uk_export_surface allowlist + contract digests + roster pins moved; three signed-difference entries (two net-new benunit columns, the would_claim_uc monotone lift).

Why (I1 evidence, receipts in data/ukds/acceptance/828-uc-capital/)

On spine-k, 1.492m weighted reported-UC benunits are blocked (0.893m would_claim_uc=false from the SPI stage-order break + 0.936m over the £16k WAS-proxy limit; FRS truth for reporters is 0.36% over). The A3 sizing receipt measured 0.454m weighted synthetic reporters carrying donor capital >£16k — 9× the ruled 0.05m threshold, so the conditional redraw stays.

Verification

Scoped funnel (UK + shared machinery files, per ruling — no US surface), tools/ci_test_groups.py --verify, and ruff check . all green, run independently by both Codex (isolated sandbox) and the reviewing session. One environment-only finding: the two licensed-cache regeneration tests (test_uk_parity_reference, test_uk_release_input_coverage_manifest) fail on a machine with cached licensed artifacts because the committed evidence pins engine 2.89.0 while the floor is 2.92.1 — pre-existing at main, untouched surfaces, owned by #749's re-pin lane.

Follow-ups (not in this PR)

  • I7: licensed spine-l rebuild + after-receipts; re-mint spine_candidate_acceptance.json and restore the acceptance-receipt binder test to strict roster equality (deliberately pinned to historical-roster-plus-exactly-this-stage until then).
  • I8: v16 recalibration, six-deferral re-measure, T0–T6 — publication-stack sequencing to be decided there (registry WS-E spine deferrals: known bugs and signed inconsistencies awaiting their increment #796; deferral window 2026-09-30).

🤖 Generated with Claude Code

…SPI (#828)

- frs_spine carries TOTCAPB4 as frs_benunit_capital behind the named -1
  unavailable sentinel (fully populated in FRS 2024-25; mapped-row count
  reported in stage evidence)
- new uc_capital_coherence stage after the last universal_credit_reported
  writer: flipped-only weighted-empirical SPI reporter capital redraw
  (children band x couple cells, identity-keyed seed) + monotone
  would_claim_uc OR-refresh; materializes uc_reported_capital
- shared column_implication gate + uk_uc_capital_coherence terminal binding
  (implication, sentinel floor, sentinel parity, same-source equality)
- derived surfaces: sources.yaml authority + regenerated projection, schema
  branch, coverage manifest, export-surface allowlist, contract digests,
  roster pins; three signed differences for the new columns and the
  would_claim_uc lift
- I1 receipts (blocker aggregates, TOTCAPB4 domain audit, A3 sizing 0.454m)
  live in data/ukds/acceptance/828-uc-capital

Implemented by Codex from the reviewed #828 plan (zero unadjudicated
deviations); licensed rebuild and recalibration follow as I7/I8.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Automated review pass (Claude Code, high effort, diff plus branch reads — no execution; test hunks and nits skipped since this is a draft). Four findings.

1. uk_runtime/uc_capital_coherence.py:~215 — the sentinel reaches the engine as a real capital amount

benunit["uc_reported_capital"] = capital.copy() is an unfiltered copy of frs_benunit_capital, so a row carrying UC_CAPITAL_UNAVAILABLE (-1.0) is handed to PolicyEngine-UK as a reported capital sum of −1.0 — a value that will be thresholded and aggregated in the UC capital test as though it were measured. Nothing between the spine and the export maps the sentinel to missing.

This is invisible on this vintage precisely because the I1 audit found the column fully populated, which is the part that worries me: the sentinel exists for a future FRS where rows are absent, and that is the build on which the defect first appears — in a stage whose receipts will say it ran cleanly. The mapped-row count in checkpoint evidence announces that sentinel rows exist, but nothing stops them being consumed.

Related, in the same file: the redraw's available = capital > UC_CAPITAL_UNAVAILABLE correctly excludes sentinel rows from the donor pool, but never repairs sentinel-carrying base-FRS reporters — those pass straight through to the carrier. So the sentinel is handled in one of the two places it appears.

Worth considering whether an in-band magic value is the right representation at all for a monetary column that crosses into the engine. If it stays, the carrier assignment is the natural place to refuse or to map to whatever the engine's missing convention is; and a test that exercises a sentinel-carrying row would make the whole path real rather than hypothetical.

2. uk_runtime/battery_bindings.py:~350 (_evaluate_column_implication) — the threshold is applied twice, so the gate passes vacuously

threshold selects positive_ids from the person-level amounts, and is then applied again inside column_implication_gate against the aggregated 0/1 indicator (implicated = numeric > threshold). For any threshold ≥ 1, an indicator that is only ever 0 or 1 can never exceed it, so implicated is all-false and the reporter→would_claim_uc implication passes with violation_count: 0 regardless of the data.

The gate's threshold for an indicator has to be 0, independent of the amount threshold used upstream. As written the evidence reports zero violations because nothing was tested, which is the class this lane has been closing all week — and here it is the gate that certifies the PR's central invariant.

3. uk_runtime/battery_bindings.py:~370 (same_source_mismatch) — tautological against its own producer

The declared same-source invariant compares capital != carrier, but uc_capital_coherence sets uc_reported_capital = capital.copy() in a single expression. The check therefore cannot fail, and the same_source_mismatch_count: 0 receipt asserts nothing about the build. The gate's nonfinite check has the same shape — it re-derives a condition cohere_uc_capital has already raised on.

Neither is harmful, but both read as coverage in the evidence, and a later reader auditing why the carrier is trustworthy will find two green checks that were never capable of being otherwise. If the intent is to guard against a future divergence between producer and carrier, the check needs to run against independently-read bytes rather than the variable it was just assigned from.

4. uk_runtime/uc_capital_coherence.py:~250 (_redraw_spi_reporter_capital) — determinism depends on clone keying, and the donor CDF is weight-sensitive

Draws come from stable_identity_uniforms(benunit_id, …) while the donor CDF is built from the current frame's cloned, mass-split households. Two consequences worth confirming:

Clone twins agree only if cloning preserves benunit_id. If clones are re-keyed, the twins draw different capital — the failure mode age_tail avoided by keying on person_source_id, and worth being explicit about here for the same reason.

Separately, the donor CDF is built by searchsorted over a cumulative weight whose weights halve on clone, so it is order- and vintage-sensitive: the same benunit can select a different donor between builds even with an identical seed, while the receipt describes the redraw as deterministic. Deterministic-given-the-frame is a weaker property than the receipt implies, and it is the kind of distinction that matters when a later build diff is being explained.


1 and 2 are the ones I would resolve before this leaves draft. 2 is a live defect on this build — the implication gate cannot fail as configured. 1 is dormant on this vintage by luck of the data, which makes it the more likely of the two to ship.

)

- test_contract.py: gate-battery digest mirrors moved in lockstep with
  contract.py; exact-k terminal fixture gains the uk_uc_capital_coherence
  entry and its column_implication detail block
- signed-difference evidence anchors re-pointed at the committed
  experiments/828-uc-capital-receipts.md (the .codex-work copies are
  git-excluded, so CI could not resolve them)
- review finding 2: binding de-conflates the person-amount threshold from
  the aggregated-indicator comparison (pinned 0.0), refuses negative
  thresholds, records amount_threshold in details; regression test proves a
  violation fires at a nonzero amount threshold
- review findings 3/4: guarded-surface intent documented (the terminal
  capital checks guard stages between the producer and the boundary), and
  the ordering/determinism contract stated on the coherence module

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

Copy link
Copy Markdown
Collaborator Author

Disposition of the review round (commit 9003087), finding by finding.

Finding 1 — sentinel reaches the engine as a real amount: refuted on the main claim, with the engine's own sources. −1 is not an in-band magic value we invented — it is policyengine-uk's documented missing convention for this exact variable (uc_reported_capital.default_value = -1, introduced with the capital-limit seam in policyengine-uk#1572), and every engine consumption point masks it: uc_assessable_capital routes on reported_capital >= 0 (a −1 benunit falls to the residual household proxy, never thresholded as measured), and household_uc_reported_capital sums reported_capital * (reported_capital >= 0) * is_benunit_head, so a sentinel contributes exactly zero to the residual subtraction. The path is exercised, not hypothetical: test_engine_uses_reported_capital_and_sentinel_routes_to_residual_proxy (requires_uk, live engine) asserts a −1 benunit routes to the proxy. On the sub-point: a base-FRS reporter with genuinely absent capital keeping −1 and falling to the proxy is the adjudicated D5 behavior ("−1 reserved for genuine absence; the household proxy remains the fallback for genuinely unobserved capital" — the issue's own contract), not an unhandled second site.

Finding 2 — double-applied threshold: correct as a latent hazard, fixed; refuted as a live defect. As shipped the binding's threshold is 0.0 and the gate demonstrably fails (the pre-existing binding test flips would_claim_uc and asserts the violation fires), so it was never vacuous on this build. But the conflation was real: any future configuration ≥ 1 would have gone silently green. Fixed in 9003087 — the configured threshold now filters only the person-level amounts, the aggregated 0/1 indicator is always compared at 0.0, negative thresholds refuse, amount_threshold is recorded in the gate details, and a regression test pins a violation firing at threshold 100.

Finding 3 — tautological same-source check: partially refuted, intent now documented. Against its producer the equality check indeed cannot fail — but the producer is not what it guards. Six stages run between uc_capital_coherence and the terminal boundary (CGT cloning and donor stacking, salary sacrifice, student loans, age tail, assembly), any of which could rebuild a benunit table and corrupt one column without the other; the terminal re-derivation is the fence against that, and byte-level re-reading belongs to the certification lane, which already verifies the written H5's identity join. The binding docstring now states this so the green receipts read as what they are.

Finding 4 — clone keying and weight-sensitive CDF: refuted on stage ordering. The redraw runs before cgt_incidence_clone, so at draw time there are no clones, no re-keyed ids, and no mass-split weights — twins copy the already-drawn values byte-for-byte, which is the same guarantee structure age_tail's keying concern was about, obtained by ordering instead of key choice. On determinism: agreed the property is twin-reproducibility given identical frame + declared seed (the WS-E determinism ladder's definition, same as every seeded stage); the module docstring now says exactly that rather than implying vintage-invariance.

Also in 9003087, the CI failures: the gate-battery digest mirrors and terminal fixture in microcosm-data/tests/test_contract.py moved in lockstep with the contract, and the three new signed-difference evidence anchors now point at the committed experiments/828-uc-capital-receipts.md instead of the git-excluded .codex-work staging copies. Full data-shard contract file + the touched UK surfaces: 517 passed locally; CI re-running.

🤖 Generated with Claude Code

@juaristi22
juaristi22 marked this pull request as ready for review August 31, 2026 18:45
@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Verification pass over 90030870 (Claude Code, high effort; no execution).

Taking the refutations first, since two of them corrected me.

The sentinel finding was wrong and I accept it fully: -1 is policyengine-uk's own missing convention for uc_reported_capital, masked at every consumption point, with a live-engine test proving the routing. I read an in-band magic value where there was a documented contract, and the D5 adjudication covers the sub-point too.

Finding 3 was also wrong, and I checked rather than just conceding. same_source_mismatch reads two distinct columns (capital_column, carrier_column) off the terminal target frame through independent pd.to_numeric conversions — not two references to one expression. So a stage between uc_capital_coherence and the terminal boundary rebuilding the benunit table and corrupting one column without the other would show up. It is a terminal re-derivation, as you said, and the docstring now makes that legible.

The two fixes verify:

  • Double-applied threshold. battery_bindings.py:370 is the only call to column_implication_gate in the binding and passes threshold=0.0 as a literal, so no path can leak the amount threshold into the indicator comparison. amount_threshold is recorded in details before the spread order could clobber it (**dict(result.details) comes first, and the primitive records its own threshold = 0.0 separately). The negative refusal sits ahead of every use of threshold and is reachable from parameter overrides — the new test drives that path at 100.0.
  • Evidence anchors. All three new evidence fields point at the committed experiments/828-uc-capital-receipts.md, with headings that exist in the new file.

Two residuals.

1. uk_runtime/battery_bindings.py:391 — the sentinel floor lets non-sentinel negatives through all four capital checks

The floor is capital < sentinel, i.e. < -1. So a corrupted negative such as -0.5 clears the floor, is not isclose to the sentinel on either side so parity passes, is finite so the non-finite check passes, and passes same_source_mismatch whenever the carrier carries the same corrupted value. Garbage negative capital slips through the whole set.

Given how carefully the −1 contract is drawn — exactly one reserved negative, everything else non-negative — the floor should encode that: equals the sentinel, or >= 0. As written it admits the open interval between them, which is the one region the contract has no meaning for.

2. uk/spine_swap_signed_differences.json:537 — one anchor still points at git-excluded staging

magnitude_provenance cites .codex-work/828_before_ab.json and .codex-work/828_before_c.json. It is the last remaining .codex-work reference in the file, so the evidence-field sweep caught three of four sites; this magnitude's provenance is unreproducible from the repo. Citing the committed receipt, as the evidence fields now do, closes it.


1 is the one worth fixing before merge: it is a fence around a documented contract that currently admits the values the contract does not define.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

First licensed measurement of this fix: spine-l + seam run v16 (2026-09-01)

Ran I7's licensed rebuild and the I8 recalibration against the six signed target-fit deferrals, on María's direction. Everything below is measured, with receipts local to the licensed store (paths given; no licensed microdata leaves the machine).

What was run

  • Stack: uk-publication-stack-829 (pushed) = this PR's head 90030870 (already containing post-Assemble the certified UK national candidate into a publishable release (#806) #812/K=1 rowwise pilot machinery: clone the spine, assign the ladder, measure per-area support (#761) #815/Add generic monetary target primitives #825 main) + three cherry-picked increments from the retired uk-publication-stack-812: the schema-2 target-fit deferral register (6 receipts, adjudication WS-E spine deferrals: known bugs and signed inconsistencies awaiting their increment #796, window 2026-08-30 → 2026-09-30), the scorer fixes, and a vintage-digest re-pin over the union at f54468b5. One consistency check fell out of the merge: this PR and the deferral register each moved only their own certification part digest — the calibration_seam part digests agreed byte-for-byte across both lines, so the union re-pin touched just the three top-level pins. Affected-suite funnel green (8 suites, 222 tests: terminal gates, contract pins, scorer, data contract/release, seam, certification, assembler).
  • spine-l (full licensed build from the stack, engine 2.92.1): data/ukds/acceptance/spine-l-828/, input sha 5176e6ec…. 14/14 spine gates passed (roster +1 for the new stage's health gate), blocked_at_phase: null. Frame shape identical to spine-k: 113,649 / 61,211 / 52,846 rows, household weight total 29,247,433.
  • Seam run v16: tools/calibrate_uk_national_dataset.py on spine-l, 1,500 epochs / family_equal, frozen scoring register pin 183acf2556ae accepted without drift — this PR leaves the compiled register surface untouched, as designed. Receipts: runs/uk-623-first-calibrated/spine-assessment-v16/.

Stage evidence (spine-l build record)

uc_capital_coherence: post-fill reporters 3,574 (records); SPI-channel redraws 1,362; would_claim_uc monotone refreshes 599; uc_reported_capital nonzero share 0.845. The I2 audit's prediction held — zero sentinel rows on this vintage.

What moved (v15/spine-k → v16/spine-l, weighted)

Cell Target Initial support Final Error
dwp.uc.households 6,758,889 4,264,244 → 5,123,151 (+859k) 5,519,753 → 5,835,515 −18.3% → −13.7%
…single_no_children 3,191,316 2,310,221 → 2,758,642 2,969,171 → 3,189,009 −7.0% → −0.1%
…single_with_children 2,213,760 893,072 → 997,023 1,426,732 → 1,468,947 −35.6% → −33.6%
…children_1 1,269,512 649,491 → 764,701 893,790 → 931,392 −29.6% → −26.6%
…children_2 1,103,880 580,941 → 681,216 709,576 → 732,529 −35.7% → −33.6%
…children_5_or_more 77,894 41,585 → 48,258 55,229 → 55,203 −29.1% → −29.1%

Solve quality held: loss 0.02545 (v15: 0.02538), 90.9% of 364 bound targets within 10%.

Findings

  1. The fix delivers what it claimed. +859k initial UC support (~58% of the 1.492m blocked reporters from the I1 receipt — the remainder overlapped both blocking causes), and the childless side of the caseload now closes: single_no_children fits exactly.
  2. The with-children residual is now cleanly attributed to Port UK compact source stages and 2024-25 refresh from uk-data #145, not capital blockage. The unblocked reporters ran ~2:1 childless (+330k of the +859k reached with-children cells), matching the 0.55/0.70/0.85 lever-probe prediction. The four with-children receipts stay in force at −26.6…−33.6%; the remaining deficit is the draw-quality shape (who gets drawn eligible), owned by Port UK compact source stages and 2024-25 refresh from uk-data #145.
  3. One receipt retires by anti-rot. hmrc/state_pension_income_band_50_000_to_70_000 relaxed to +24.7% (inside the bound) as this fix relieved the solver pressure it covered, and the gate failed it as stale exactly as designed — the register cannot outlive its defects. The private_pension_income_count_100k–150k sibling stands at +25.6%.
  4. One new 0.07pp breach needs adjudication: obr.capital_gains_tax at −25.1% (it has hovered −24.8…−25.1% across every vintage since v12; spine-l's re-realized draws landed it a hair outside). Unsigned — María's call whether it takes a seventh receipt (same window, WS-E spine deferrals: known bugs and signed inconsistencies awaiting their increment #796) or routes to UK CGT top-tail age and geography joints inherit from incidence and look off — benchmark against HMRC marginals #725's CGT top-tail workstream.

Next

On the pending adjudication: remove the stale state-pension receipt (+ CGT disposition) → v17 → the post-#828 staging artifact for the T0–T6 campaign re-run. The remaining I7 debris (acceptance-receipt re-mint, binder-test strict-roster restore) is untouched by this measurement.

Run by Claude (Fable 5) at María's direction; measurements from the signed build/seam receipts named above.

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Third pass (Claude Code, high effort). Head is unchanged at 90030870, so this covers the two open residuals and then the licensed measurement, since the receipts are the durable artifact here.

The two residuals are both still open

(a) battery_bindings.py — the sentinel floor. Unchanged: below_floor = np.isfinite(capital) & (capital < sentinel). A value of -0.5 is finite, is not < -1, is not isclose to -1 on either side so sentinel_mismatch is False == False, and equals its carrier whenever both columns carry the same corrupted value. All four capital checks still pass on it. The equals sentinel or >= 0 predicate closes the open interval the −1 contract has no meaning for.

(b) spine_swap_signed_differences.json:537 — still cites .codex-work/828_before_ab.json and .codex-work/828_before_c.json. grep -n codex-work over the file returns exactly this one line, so the sweep caught every other site and this magnitude's provenance remains unverifiable from the repo.

On the licensed measurement

The run itself is a real milestone and the movements are genuine. But several of the things presented as corroboration cannot come out any other way, and the attribution is not established — which matters because this write-up is what a later reader will cite.

1. Three of the reported controls are entailed by what the change touches. The fix preserves and re-anchors column values on an existing frame: it adds and drops no rows, and household weight total 29,247,433 is a control total the seam is constrained to hit. So identical row counts (113,649 / 61,211 / 52,846) and an identical weight total could not have come out otherwise. "Frozen scoring register pin 183acf2556ae accepted without drift" likewise verifies that a pinned file is still pinned. This is the same shape as the bit-identical support control on #815 — worth labelling these as invariants rather than listing them where they read as four independent confirmations.

2. No control separates the fix from a re-solve at a moved starting support. The report states initial support moved substantially on every row, and the seam then re-solved 1,500 epochs from there. Under that setup, any perturbation of comparable magnitude buys some error reduction whether or not it is semantically correct, so the v15→v16 movements are equally consistent with "the optimiser got an easier starting point". One of two runs would settle it: spine-l scored at v15's initial support (fix isolated, seam held), or spine-k re-solved from a null perturbation of equal size (seam isolated, fix held).

Related: the six cells are not six independent corroborations. Under family_equal, single_with_children / children_1 / children_2 overlap heavily in households — and single_with_children and children_2 both landing on −33.6% is a sign of that overlap rather than of two measurements agreeing.

3. children_5_or_more unchanged at −29.1% is the one cell that could falsify the story, and it is the one left unexplained. Every sibling moved 2.0–6.9 pp; this moved 0.0 pp to the reported precision, while its inputs allegedly moved and the seam re-solved for 1,500 epochs. Three readings are available and they are not equivalent: the cell is not fed by the changed capital path (which falsifies "every row"); or it is pinned by a binding constraint or small-cell floor (in which case it is not a measurement of this fix and should be excluded rather than listed); or the row is a stale carry-over from the v15 table. Worth resolving explicitly.

4. "Zero sentinel rows on this vintage" makes 14/14 and blocked_at_phase: null unfalsifiable for exactly the code under review. With no sentinel rows present, below_floor, sentinel_mismatch and the sentinel branch of the fill are all inert — the gate could not have failed on the paths this PR adds. The pass is real but empty with respect to the new behaviour, and it also means residual (a) went unmeasured.

Relatedly, uc_reported_capital nonzero share 0.845 with zero sentinel rows means the remaining 0.155 are exact zeros, and nothing distinguishes "genuine zero capital" from "fill produced zero" — which is the fix's central claim. A run on a vintage that does contain sentinels, plus a case around −0.5, is what would give the measurement teeth.

One framing note. After the fix the aggregate dwp.uc.households error is still −13.7% and single_with_children still −33.6%. The movement is real and worth having, but "first licensed measurement" should not be read — or written up — as validation of the caseload, which remains badly under-hit.

…829 round 2)

- residual (a): the -1 contract defines exactly two regions (the sentinel,
  nonnegative amounts); enforce that domain at both layers — stage-time
  refusal in cohere_uc_capital (and the donor filter tightened to >= 0) and
  the terminal binding's capital AND carrier checks, since a corrupted pair
  like -0.5/-0.5 legitimately passes the sentinel-parity and same-source
  checks. Regression tests pin the -0.5 case at both layers; gate details
  rename below_floor_count to capital/carrier_domain_violation_count with
  the fixture mirror moved in lockstep.
- residual (b): the uc-reporter-claim-refresh-lift magnitude_provenance now
  cites the committed experiments/828-uc-capital-receipts.md; zero
  .codex-work references remain in tracked files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@juaristi22

juaristi22 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Round-2/3 disposition (commit 880b0d1). Both residuals closed; the measurement points are answered only where they touch this PR's code, since the licensed run and its write-up came from the measurement session, not this one.

Residual (a) — the undefined negative interval: fixed at both layers. Your predicate, applied twice over. The terminal binding replaces the bare floor with the domain check — a finite value must be exactly the sentinel (isclose) or ≥ 0 — on both uc_reported_capital and frs_benunit_capital, since a corrupted pair like −0.5/−0.5 legitimately passes sentinel-parity and same-source equality (that being precisely why the interval needed its own check). And the stage itself now refuses the interval at entry: cohere_uc_capital's validation rejects any finite value that is neither the sentinel nor ≥ 0, so garbage negatives can't flow between the producer and the terminal boundary in the first place; the donor filter tightened to >= 0 accordingly. Two regression tests pin your exact −0.5 case — the binding test additionally asserts same_source_mismatch_count == 0 on it, documenting that the older checks pass on the corruption and the domain check is what catches it. Gate details rename below_floor_countcapital_domain_violation_count/carrier_domain_violation_count, with the contract fixture mirror moved in lockstep. This also answers the code-side half of your measurement point 4: the −0.5 path no longer waits for a sentinel-bearing vintage to be exercised — it is a committed unit case at both layers.

Residual (b) — the last staging citation: fixed. uc-reporter-claim-refresh-lift's magnitude_provenance now cites the committed experiments/828-uc-capital-receipts.md (Part B) and names the licensed-side location of the raw JSONs; the register test's pinned literal moved with it. grep codex-work over the register — and over every file this PR touches — returns nothing. For the record, a repo-wide grep does return 22 pre-existing references in files this PR does not touch (mostly the local-target census lane, e.g. uk_local_target_census.json citing .codex-work/consumer_facts_uk.jsonl) — same unreproducible-provenance class, different lane; flagging it for a follow-up issue rather than folding unrelated files into this diff.

Verification: 493 tests across the touched surfaces plus ruff, green locally; CI re-running on 880b0d1.

🤖 Generated with Claude Code

@vahid-ahmadi

Copy link
Copy Markdown
Contributor

Verification pass over 880b0d17 (Claude Code, high effort; diff-only, no execution).

The domain fix holds. ~(isclose(x, sentinel) | x >= 0) on finite values correctly refuses -1e-12, -0.5 and the whole open interval; -0.0 and +0.0 pass via >= 0; NaN and ±inf are caught by the pre-existing nonfinite term so they cannot slip through the negated predicate. Both columns are covered in the binding, the stage refuses at entry, and the tightened donor filter cannot drop sentinel rows — a sentinel-valued row was never a donor under the old > -1 either. The key rename is consistent: a repo-wide search for below_floor_count returns zero hits and the contract fixture mirror moved in the same commit. Applying the predicate at both layers rather than only the terminal binding is the stronger reading of the finding, and the regression asserting same_source_mismatch_count == 0 on the −0.5 case is a good way to record why the domain check is the one that catches it.

The citation resolves too: experiments/828-uc-capital-receipts.md exists at this commit with a ## Part B — A3 sizing receipt heading matching the reference, and zero codex-work occurrences remain in spine_swap_signed_differences.json.

Two findings.

1. battery_bindings.py:391 and uc_capital_coherence.py:121isclose leaves the top sliver of the interval open

np.isclose(capital, sentinel) with default tolerances accepts roughly [-1.00001, -0.99999] as "exactly the sentinel" (atol 1e-8 plus rtol 1e-5·|−1| ≈ 1.001e-5). So a corrupted -1.000005 passes both layers, and is then silently treated as unavailable by the >= 0 donor filter — i.e. the one region of the interval that still gets through is the region that gets read as the sentinel rather than refused.

Small, but the error message says "exactly the {sentinel:g} sentinel", and the whole point of the round was that the contract defines two regions and nothing between them. An exact capital == sentinel — or a tolerance far tighter than the amount grain — makes the code match the message. The sentinel is a fixed literal, not a computed float, so exact comparison is safe here in a way it usually would not be.

2. The commit message's repo-wide claim is broader than what landed

"Zero .codex-work references remain in tracked files" is not true at 880b0d17: uk/target_reference_membership.json:2 carries "source_fact_feed": ".codex-work/consumer_facts_uk.jsonl", and uk_runtime/local_target_census.py:83-84 still cites git-excluded paths as provenance.

The narrow PR-scoped claim — the last citation in spine_swap_signed_differences.json — is satisfied, and that was my finding. Flagging only because the broader phrasing will read later as a completed sweep, and these two are the same unverifiable-provenance shape in files a reader would expect the sweep to have covered.


Nothing else qualifying in the three runtime hunks — no inverted condition, removed guard, falsy-zero or dead code left behind. 1 is worth a one-line change; 2 is a commit-message correction rather than a code fix, though the two remaining citations are worth their own pass sometime.

@juaristi22

Copy link
Copy Markdown
Collaborator Author

Measurement-session response to the third-pass review (points 1–3, 4's measurement half, and the framing note)

All from the measurement side, on uk-publication-stack-829 at f54468b5 — no changes to this PR's tree; existing v15/v16 receipts plus two read-only probes of spine-l. (These predate 880b0d17; on this vintage both capital columns carry zero finite negatives ≠ −1, so the tightened donor filter selects an identical donor pool and the fill is unchanged by it.)

Point 1 — conceded. Row counts and the spine weight total are invariants of a column-only stage, and the register pin verifies this PR's no-register-change contract rather than corroborating the fix. The write-up should have labelled them as preconditions; the ledger copy will.

Point 2 — the isolation run you asked for is already in the table, and the fuller version strengthens it. The "initial support" column is materialized at seam entry, before the solver runs — it is run (i), fix isolated, solver held out, by construction. The complete initial-layer decomposition across the composition surface:

Cell init err v15 → v16 Δ init fin v15 → v16 Δ fin
households (aggregate) −36.9% → −24.2% +12.7pp −18.3% → −13.7% +4.7pp
single_no_children −27.6% → −13.6% +14.1pp −7.0% → −0.1% +6.9pp
couple_no_children +36.1% → +89.7% +53.6pp +0.0% → +0.1% +0.1pp
couple_with_children −23.3% → −5.3% +18.0pp −5.9% → −0.0% +5.9pp
single_with_children −59.7% → −55.0% +4.7pp −35.6% → −33.6% +1.9pp
children_1 −48.8% → −39.8% +9.1pp −29.6% → −26.6% +3.0pp
children_2 −47.4% → −38.3% +9.1pp −35.7% → −33.6% +2.1pp
children_3 −51.9% → −44.1% +7.9pp −9.5% → −2.6% +7.0pp
children_4 −54.2% → −50.5% +3.7pp −0.4% → −0.1% +0.3pp
children_5_or_more −46.6% → −38.0% +8.6pp −29.1% → −29.1% −0.0pp

Two properties a null perturbation of equal size would not reproduce: the movement is structured cell-by-cell in the reporter composition's direction (childless cells gain most; couple_no_children overshoots to +89.7% initial — the redraw adds more childless-couple support than the target wants, and the solver reins it back), and every final-layer gain is bounded by its initial-layer gain — no solver amplification anywhere. The overlap point is right and accepted: family-type and children-count are two partitions of one caseload, so the honest count is one aggregate plus two partitions, not six independent confirmations; single_with_children and children_2 sharing −33.6% is shared households, as you said.

Point 3 — resolved by the same decomposition, against all three of your readings. children_5_or_more initial support moved 41,585 → 48,258 (+8.6pp initial error) — it is fed by the changed path. Its final estimates differ (55,229 → 55,203) — not a stale carry-over. What is flat is the solver's equilibrium: under family_equal this 77,894-target cell carries a sliver of the family gradient, and the solve re-landed on the same compromise while spending the family's improvement in the siblings. So it is a measurement of the fix at the initial layer and a measurement of the objective's trade-off at the final layer; the original table conflated the two, which your reading caught.

Point 4 (measurement half) — the zero-ambiguity is measurable on this vintage, and the fill passes. Among reporter benunits on spine-l: the redrawn SPI-synthetic cohort (2,737 records) has zero-capital share 0.298 vs the carried base-FRS donor cohort's 0.307 (4,428 records), >£16k share 0.44% vs 0.54% (against the 0.36% FRS truth you cited, and the 0.936m weighted over-limit before), median positive £200 vs £175. Fill-produced zeros occur at the donor rate because the empirical draw pulls from exactly that pool — they are donor-faithful zeros, not degenerate fill. The sentinel paths remain unexercised here as you said; the 880b0d17 unit cases now carry that code-side, and a sentinel-bearing vintage stays the real-data test.

Framing note — accepted without qualification. v16 measures movement and attribution, not caseload validation: the aggregate stands at −13.7% with four receipts in force, and the with-children residual is #145's. The ledger and any later write-up will carry that framing.

The null-perturbation seam run (your run ii) remains available if the initial-layer decomposition doesn't settle the attribution for you — our read is that it does, since the initial layer is solver-free by construction, but say the word.

Measurement session (Claude Fable 5, at María's direction); interaction with this PR is comment-only — all runs live on uk-publication-stack-829.

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.

UK spine: preserve FRS benefit-unit capital for UC while retaining WAS household wealth

2 participants