Repository navigation
Calibrate the QRF sign gates out of fold, for every gated UK imputation - #1391
Conversation
03427be to
e80a424
Compare
MaxGhenis
left a comment
There was a problem hiding this comment.
Thanks María. This is a clear diagnosis, and calibrating the gate out of fold is the right fix. I'd land it after the Budget rather than in Monday's cut, since it moves 22 of the 24 excise inputs and the COICOP totals that the VAT coverage is being derived on.
Two changes before merge, and one wording fix:
- Fall back when a fold has too little weight.
_can_cross_fitcounts rows, so a target with zero-weight rows can pass it and still leave a training fold with no weight. sklearn then raisesZeroDivisionError: Weights sum to zero. A reproducer: ten donors, five per class, eight of them at weight zero. The in-sample gate fits; the cross-fitted one crashes. Please check each fold's training weight per class, and keep the in-sample gate when it falls short. The LCFS weights should all be positive, but this matters before other stages opt in. - Check convergence before returning the calibration. When the out-of-fold scores are saturated, the Newton step is huge and the line search hits its 1e-10 floor. The loop then returns the identity map with no warning, so the rate balance fails silently. A reproducer: ten rows, five per class, every logit 29. That gives a positive-class mean of about 1 against a rate of 0.5. Please check the weighted residuals at exit, and fall back to an offsets-only fit or the class rates, or raise. Real gates probably never get there, and your LCFS figures show the balance held, but it should fail loudly.
- Wording. The balance is exact on the pooled out-of-fold donor scores, not on recipients. Also, in the chain, a sign that changes moves later targets' amounts through their predictors, even though the forests and uniforms are the same. The changelogs and the module docstring should say both.
Please also add Hypothesis tests for the invariants:
- the weighted rate balance, including saturated and constant scores;
- the sparse-weight fallback;
- base-versus-head byte identity on the default path.
The two reviews (one by Claude, one by GPT-6.1 Sol) checked these, all on synthetic data:
- The default path is byte-identical to the base, and so are 36 checkpoint byte hashes across all seven regimes.
- Resuming under a different calibration is refused.
- LCFS is the only opt-in.
- The 22 UK manifest re-pins and the US seed-protocol and seed-map pins recompute.
- On held-out data the cross-fitted gate lands at about 1.0 of the true rate with 100 or more positives; the in-sample gate lands at 0.05 to 0.5.
I can send you the reproducers and the proposed property tests.
|
Review of 807e024 What I checked: staleness against main (0b1eecf), the cross-fit code in Max's review of 24 Oct still applies: there are no commits since it. Must
Should
Nit
Checked and fine
CI: none at this head. The PR is stacked on a branch that has since merged, so no checks have run; they will start once it is retargeted to main. Verdict: the method is sound and the LCFS evidence is convincing, but it needs a rebase onto main with the gambling figures re-measured, and Max's two fixes, before it can merge. 🤖 Generated with Claude Code |
807e024 to
bbef04e
Compare
|
Thanks Max. All three are in 37df309, on the branch now rebased onto main:
The Hypothesis tests cover:
Byte identity of the default path against main held on my machine: one digest over draws, chain checkpoints and positional predicts, across all seven regimes and two seeds. I haven't committed it, because those bytes are platform-specific. With the Budget behind us, every other gated UK fit now opts in too, measured the same way. The figures are in the body. The US fits stay on the in-sample gate: their donors and engine aren't on my machine, so they're yours to measure if you want them. 🤖 Generated with Claude Code |
|
Thanks Vahid. Taking your points in order:
🤖 Generated with Claude Code |
bbef04e to
5ed3468
Compare
|
Round 2 at 5ed3468 What I checked: every commit since 807e024, both replies, the Round 1 items
Must
Should
Nit
New scope, checked
Checked and fine
CI at 5ed3468: lint, wheels, select-countries and integration-uk pass. engine-uk fails on both Pythons and engine-free fails on 3.14 (Must 1). engine-free 3.13 and engine-us were still running when I posted. Verdict: the round 1 items are all closed and the wider opt-in is well covered. Once the two test fakes are fixed and CI is green, it is ready to merge. 🤖 Generated with Claude Code |
|
Thanks Vahid. All three are in, at 5c412e8:
CI at 5c412e8: engine-uk now passes on both Pythons, as do integration-uk, lint and wheels. engine-free fails on one test, 🤖 Generated with Claude Code |
5c412e8 to
66a3f08
Compare
|
Round 3 at 66a3f08 What I checked: the rebase onto main 79a533c (range-diff against 5ed3468), the three new commits, the PR body and LCFS changelog, and CI. I ran the NTS bus, company-cars and E6 identity-receipt test files at head (80 pass). Code reading and CI otherwise; no survey data. Round 2 items
Rebase and default path
No new findings. CI at 66a3f08: all green. engine-free, engine-uk and engine-us pass on 3.13 and 3.14, as do integration-uk, lint, select-countries and wheels. The transport-battery binding-identity failure you mentioned is gone now that main has #1495. Verdict: all three round 2 items are closed and CI is green. Ready to merge from my side. 🤖 Generated with Claude Code |
…sm#1359) A sign gate is a default HistGradientBoostingClassifier calibrated only on its own training rows. It memorises a rare class's training rows, so on every other row, every recipient included, its mean probability of that class falls short of the class's rate. That mean is the share of rows drawn into the class. On held-out LCFS 2024-25 donors, out of fold, the lcfs_consumption chain's gates gave education 0.42 of its rate and solid fuels 0.11. RegimeGatedQRF now takes a gate_calibration, named by GateCalibration. in_sample, the default, is the gate as before. cross_fitted fits a gate on each of five folds of the training rows, stratified by class. One weighted calibration of a slope and per-class offsets (Platt scaling, for two classes) is fitted on the gates' out-of-fold log-probabilities and maps each of them, and a row's probability is the mean of the mapped gates. On the same donors it puts every chain target's out-of-fold mean probability at its rate (education 0.997, solid fuels 1.003) and lowers every target's log loss. Regularising the gate alone, a single calibrated gate, and shifting the mean all left a gap. - The fold assignment and the fold seeds derive from the seed the in-sample gate takes, so the fit RNG advances by the same draw and every forest and draw uniform is unchanged. - A class with fewer rows than folds keeps the in-sample gate. - QRFChainState carries the calibration. It writes it into model_config only when it is cross-fitted, so in-sample checkpoints serialize as before, and a resume under another calibration is refused. qrf.py's bytes move the fit.qrf graph-parity pins, re-pinned with tools/graph_parity_repin.py; their draws are positive-only, so no gate is fitted and they do not move. They also move the spec-loader golden vector. The seed protocol re-pin follows in its own commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Max's review of 24 Oct and Vahid's of 2 Nov on microcosm#1391. - A fold with no weight for a class: the fold assignment is checked before any fold gate is fitted, and such a target keeps the in-sample gate instead of raising inside sklearn. Whether a fold falls short depends on the seed, so a property test covers any seed and any number of weightless rows. - A calibration that cannot balance: the score equations are checked at exit. Where Newton's steps stall, as on saturated or constant out-of-fold scores, damped (Levenberg-Marquardt) steps take over. If the balance still fails, every row gets the class rates. A well-behaved fit converges as before. - Grouped folds: `groups` on fit, start_chain, fit_draw_next and the fit helper deals whole groups, such as households, to folds, so a person-level fit does not score a person with a fold gate that trained on their household. A chain fingerprints its grouping, writing it only when given, and refuses a step that changes it. - Recorded outcomes: FittedRegimeGatedQRF.gate_outcomes() names the calibration each target actually got, with the new GateOutcome constants: cross-fitted; in-sample because a class is thin, or because a fold has no weight; or the class rates. - fit.qrf@1 takes gate_calibration. It writes the calibration and the outcome into the receipt only when the calibration is cross-fitted. - Wording: the rate balance is exact on the pooled out-of-fold donor scores, not on recipients, and in a chain a sign that changes moves later targets' amounts through their predictors. The tests cover Max's two reproducers, Hypothesis properties for the rate balance (saturated, constant and ordinary scores), the sparse-weight fallback, grouped folds and the default path, and the kernel option. On this machine the default path is byte-identical to main's qrf.py: one digest over draws, chain checkpoints and positional predicts, across all seven regimes and two seeds. It is not committed, because its bytes are platform-specific. The committed test checks that the default gate is exactly the classifier it always fitted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
microcosm.fit.qrf is an attested kernel module, and the gate calibration and the review fixes change its bytes, and those of microcosm.fit.kernels. These legitimately move, as they did in 1734b9e: - the seed protocol's content digest; - the compiled seed stream map digest; - the spec-loader golden vector, which attests the fit kernels' bytes; - the fit.qrf@1 graph-parity pins, its implementation hash and the three platforms' node keys, re-pinned with tools/graph_parity_repin.py. Its draws are positive-only, so no gate is fitted and the direct bytes do not move. Main's own qrf.py matched main's pins before the change, and the US spec is unchanged. The committed F0 coverage report is regenerated from the re-pinned inventory, and tools/spec_engine_coverage.py --check passes. The US bundle hash that the multispine pool tool test pins moves with it, to the value the regenerated report carries. Only CI's engine-us and engine-workflow tiers check the US pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lcfs_consumption chain declares gate_calibration: cross_fitted on its fit_weighted_qrf_chain, so each gate draws a spending at its donor rate on frame households, which the gates were never fitted to. The in-sample gates drew rare spending there too rarely: on held-out donors, solid fuels about a tenth as often as they occur and education under half as often. The forests and the keyed uniforms are unchanged. A stage-only run on microcosm#1333's test spine (78,440 households, prior weights) passes every LCFS stage-health gate. The share of households with each spending moves as follows, with the LCFS donor's share in brackets: - solid fuels 0.33% -> 2.25% (2.47%); - liquid fuels 3.09% -> 3.88% (4.54%); - education 4.31% -> 6.65% (5.83%); - tobacco 8.16% -> 11.16% (10.34%); - gambling stakes 14.63% -> 16.70% (17.03%); - betting and gaming 1.96% -> 2.63% (3.58%). The near-universal divisions' zeros are now drawn at their rate too, so their incidence moves towards the donor's. The twelve divisions' prior-weighted total falls 1.4%, and the seven divisions held at the shared frame-to-donor ratio fall with it. The excise inputs keep their national levels, but the multiples of spending change. Tobacco duty is about 1.8 times tobacco spending, down from 2.2, and the other gambling duties are 5.5 times betting and gaming stakes, down from 6.4. Lottery Duty is 0.61 times National Lottery stakes, up from 0.56, and alcohol duty 0.45 times alcohol spending, up from 0.41. The stage notes carry these figures and the cross-fitting. The derived files follow the declaration: source_stages.json, the coverage manifest's source digest, the schema's enum on the operation and the charter-H2 fixture. The H2 fixture changes only in the stage's notes and the new parameter. A control with the new qrf.py and no declaration reproduces the stage's earlier run byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1359) These now declare gate_calibration: cross_fitted: - the ETB services and WAS wealth chains; - the SPI income stages: the tape forest, the FRS reference forests of its rank transport, and stage 2; - the SPI housing shell's amounts, the NTS bus-use band, salary sacrifice and the Universal Credit reporter redraw; - the LCFS prepayment-meter and zero-road-fuel redraws. The FRS reference forests take stage 1's calibration, so the gate that places a twin's sign and the gate that reads it are calibrated alike. Fits on people or benefit units pass their households as fold groups. The new uk_runtime/qrf_gates.py reads the declarations and records each gate's outcome in the stage evidence. The ETB VAT rate is positive on every donor, so that fit has no gate and stays undeclared. Re-derived from the declarations: source_stages.json, the release-input coverage manifest and the H2 parity fixture. In the fixture, the eight stages' contract hashes move, and so do the synthetic facts it derives from its own draws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
María's ruling of 2026-11-03. The gambling participation gate (uk_stage_lcfs_consumption_gambling_participation) bounds the scale that brings the added regular players' drawn stakes to the seen players' mean. With the LCFS chain's sign gates cross-fitted, the chain draws betting and gaming for more of the diary's households, at about the donors' own stake per player. The seen mean falls, and the scale with it, from 0.63 to about 0.41, under the old floor of 0.5. The stake draws do not move. Nor do the keys' GSGB participation or the duty bases' published levels, which other parts of the gate check. The battery's mirrored digests are re-pinned: the UK gate battery's policy, manifest and spec fingerprint, and the spine certification part's two digests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…crocosm#1359) - The notes of the seven stages f32268e86 opts in name the cross-fitted gate and, where the fit is on people or benefit units, its household folds. - The LCFS notes' duty multiples are the ones the cross-fitted chain gives: alcohol duty about 0.44 times alcohol spending, tobacco duty about 1.8 times. - The LCFS changelog fragment is re-measured on full spines from main, with the gambling top-up in place, control against head. The UK fragment adds the held-out and frame results. Re-derived: source_stages.json, the coverage manifest, and the notes in the H2 fixture. The stage contract hashes do not cover notes, so uk_spine.json does not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main added three fakes, or kept them, after this branch's opt-in. They stand in for the QRF of a stage that now constructs it with gate_calibration= and fits it with groups=: - the company-cars test's salary-sacrifice forest, whose fitted model also needs gate_outcomes() for the stage's evidence; - the SPI pension-bridge test's CapturingQRF, which now passes groups on to the shared fake; - the E6 identity receipt's chained model, which ETB services builds. CI runs engine-free and engine-uk with --maxfail=1, so at 5ed3468 it stopped at the first two; the third failed only locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rocosm#1359) The band fit passed household groups only when the donor carried household_id, and otherwise fell back to row folds without saying so. Row folds split a household's members between fold gates, the leak household folds close. The production donor carries the column, so nothing moves; a cross-fitted fit on a donor without it now raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e-uk#2634 (microcosm#1359) The prior-weighted full-rate VAT base falls 1.7%. The coverage that matches receipts moves by about that ratio, from whatever value the cut it is re-derived on starts at: 0.69 under PolicyEngine/policyengine-uk#2634, derived on a6299504, which gives about 0.68. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… payments (microcosm#1426 review) Vahid's third round on #1426 left two nits on spi_benefit_coherence, and #1426 merged without them. Neither contributory award is reduced for the copy's pension payments, though the copies carry the SPI tape's (private_pension_income, from PENSION). JSA Regs 1996 reg 81 takes every pound above GBP 50 a week off contribution-based JSA, and ESA Regs 2008 reg 74 half of every pound above GBP 85 a week off contributory ESA. B10 tests the copy's work alone, so the awards stand. The receipt now gives, for each award, the support and weighted award of the copies whose pension payments are above its threshold, the most a reduction could take off. A figure is withheld where one or two source households carry it or the rest of the award. JSA's threshold is read from policyengine-uk's gov.dwp.JSA.contrib.pension_disregard with the other JSA limits. ESA's is a constant, because policyengine-uk has no parameter for it. The node declares private_pension_income, so the H2 fixture moves. The docstring also says that the weekly work tests average the tape's annual pay over 52 weeks, which understates part-year claimants, above all of contribution-based JSA, which lasts at most 182 days (Jobseekers Act 1995 s. 5(1)). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66a3f08 to
70bd560
Compare
This PR's CI passed at 01:5xZ against main f8f543a. Main has since taken six PRs (#1345, #1493, #1467, #1426, #1465, #1391), 178 files, none of them this PR's. #1493 changes how the target-compilation kernels hash their code, so CI reruns here on today's main before the PR lands. The merge is clean over the single base f8f543a, and against main it changes only this PR's own lines, file for file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This PR's CI passed at 01:5xZ against main f8f543a. Main has since taken six PRs (#1345, #1493, #1467, #1426, #1465, #1391), 178 files, none of them this PR's. #1493 changes how the target-compilation kernels hash their code, so CI reruns here on today's main before the PR lands. The merge is clean over the single base f8f543a, and against main it changes only this PR's own lines, file for file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main brings #1426 (SPI age bands and #1095's B3 to B10 items, with HMRC's two High Income Child Benefit Charge rows), #1391 (QRF gate calibration) and the rest merged since 3 November. Resolved by hand: - uk_population_targets.json keeps both sides' rows: the five company car rows, then the two HICBC rows, in the targets and the registry scope, and both sides' unmapped declarations; - uprating_holds.json keeps both sides' holds; - the counts in test_uk_population_targets.py (467 targets, 405 in the registry scope, 220 unmapped declarations) and test_uk_target_references.py (1,425 active references) add both sides' rows. Regenerated on the merged tree, after syncing the venv to the merged lock: - the target references and their membership, on Chronicle 08962f1. Against main only the five car rows are new; no existing row changes; - the national compile-parity receipt; - the release input coverage manifest: the source-manifest hash, and company_cars' two pay columns; - the H2 UK spine fixture: company_cars' declaration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven PRs reached main after this PR's last CI run, which tested 52fc34c on main f8f543a: #1345, #1493, #1467, #1426, #1465, #1391 and #1473. None of them changes a file this PR changes, and git merge-tree writes tree ed52268 with no conflict. This merge starts CI on main as it now stands, including the new installed-emitter job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…umer-notes Brings in the vehicles and Head Start layer, which now carries main 0f0e777 (through #1473, including #1496 and #1391) through the receipt-anchor layer. The merge is clean. The reviewed-fill register changes no attested US kernel bytes, so every pin stays main's and current: repin_us_spec_engine_contracts --check, generate_us_bundle_from_constants --check (spec_sha256 a6bb8fc4), spec_engine_coverage --check (42210/42210 fields, 41/41 inventory checks), repin_us_pool_engine_contracts --check, and the release input coverage manifest regenerates byte-identical. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Follows Vahid's review of #1381 (#1359). Rebased onto main (d4d66a9), and retargeted from #1381's merged branch. It also carries Vahid's two round-3 nits on #1426, which merged without them (see "#1426's nits", below).
Microcosm's regime-gated QRF decides whether each recipient row is zero or positive (or negative) with a boosted classifier, the sign gate. The gate is calibrated only on the donors it was fitted to, so on every other row it draws a class at the wrong rate. On the LCFS donors it draws rare outcomes far too rarely; on the SPI stage 2 it over-draws the rarest benefit reports.
This PR adds an opt-in that calibrates each gate out of fold, and turns it on for every UK fit that has a gate. The US fits keep today's gate, and their draws do not move.
How the QRF model changes
RegimeGatedQRF(gate_calibration=...)takes a name from the newGateCalibrationconstants.in_sample, the default, is the gate as before, byte for byte.cross_fittedworks in four steps:groups, it deals whole groups, such as households, to folds.Fallbacks, each recorded:
in_sample_thin_class).in_sample_weightless_fold).cross_fitted_class_rates).FittedRegimeGatedQRF.gate_outcomes()names what each target got, and every opted-in stage writes it to its evidence undergate_outcomes.What does not change:
fit.qrf@1graph kernel takes the option too.Where it runs
Each stage declares
gate_calibration: cross_fittedon its operation, and the schema allows it as an enum:Fits on people or benefit units pass their households as fold groups. The ETB VAT rate is positive on every donor, so its fit has no gate. The gate is bypassed, and nothing moves, in gambling participation, the property components, the LISA amounts and the LCFS gas payers.
Gate level: every gated UK fit, out of fold on its own donors
Method:
RegimeGatedQRFfits them.Nine fits whose rarest class has fewer rows or households than folds are left out, since the build keeps the in-sample gate there anyway. No fit below rests on fewer than three donors in its rarest class.
Within 10% of the rate, in sample → cross-fitted, and fits with the lower log loss when cross-fitted:
Overall:
The LCFS gates are the most overfit: with fewer than 10,000 donors, the classifier has no early stopping.
Frame level: two full spines from main, control against this head
Both builds use the same recipe, on main 4a36edc and on this branch as it stood on it. The branch has since been rebased onto d4d66a9. None of the 286 commits between touches microcosm-fit. The first 152, up to 79a533c, touch no stage this PR opts in either: 119 touch none of the UK build or microcosm-data, and the 33 after c8984c4 change the UK build's diagnostics, staging and publication code, the data contract's diagnostics checks and the frame's concept mapping. Of the 134 since 79a533c, #1426's change two stages this PR opts in:
lcfs_consumption(the zero-emission share by income fifth, each road fuel levelled to its household share, and the vehicle excise duty inputs) andhmrc_spi_income_spine(the stage-1 forests' age band and the employment pay floor). The spines were not rebuilt on any of them, so the figures below are this PR's move on 4a36edc. Shares are of frame households or people, at prior weights, with the donor's weighted rate in brackets. The control passes all 55 spine gates. The head passes all 55 with the gambling stake-scale floor at 0.3 (see "The gambling stake scale", below).LCFS consumption, with
top_up_gambling_participationin place:Rare spending:
The near-universal divisions' zeros are now drawn at their rate. Incidence moves to the donor's:
Totals:
Excise inputs. They keep their national levels; who carries them changes:
The last two keys are carried at the top-up's participation, 62.7% and 29.8% of households, in both runs. The betting and gaming multiple now rises, where on the pre-top-up stage it fell: the top-up sizes the added players' stakes to the seen households' mean, and that mean falls.
The other stages:
spi_benefit_coherencethen zeroes or restores from the twin for SPI rows.The gambling stake scale
One gate needed a new bound:
uk_stage_lcfs_consumption_gambling_participation. Betting and gaming's amount scale is 0.41 in the head, against 0.63 in the control and the old floor of 0.5. The floor is now 0.3, on María's ruling of 2026-11-03. The scale is the chain's level times the walk's selection:The stake model, and the walk that picks players, barely move. What moves is the chain's level: it now draws betting and gaming for 38% more households, with smaller stakes each.
Lowering the floor hides no move away from the official figures, because neither depends on the scale:
What the scale compares is the chain's diary-level stakes against the stake model's draws for the same households.
VAT coverage
Max's review pointed out that this moves the COICOP totals the VAT coverage is derived on. It does, by about 1.7%.
policyengine-uk divides household VAT by
survey_consumption_coverage, 0.67, times the household share of receipts, 0.7. Its base is the twelve COICOP divisions times each household's full-rate share. Ona6299504, the cut policyengine.py certifies now, 0.67 puts 2024-25 VAT 3.0% above receipts, and Max's open PolicyEngine/policyengine-uk#2634 re-derives it there as 0.69.On the two spines, at prior weights:
So read the move as a ratio. At any given coverage, VAT on a release built with this PR comes out about 1.7% lower, and the coverage that matches receipts falls by about the same ratio. The 0.676 and 0.665 are these spines' own levels, at prior weights, not a certified cut's. On that basis, if the 1.7% carries over, VAT at 0.69 on the first cut built with this PR comes out about £2.8bn below 2024-25 receipts, and re-deriving there gives about 0.68. The parameter's note asks for it to be re-derived when the dataset it matches changes, so that starts from whatever value is then current.
obr.vatstays out of the calibration objective, so calibration does not pull it back, though the bound road-fuel row may recover part of the transport share. The exact figure needs a calibration of each spine.Read before reviewing
The byte edits of
qrf.pyandkernels.pymove:fit.qrfgraph-parity pins;Only CI's engine-us and engine-workflow tiers check the US ones. The seed pins, the F0 report and the pool hash are recomputed on 79a533c. Main re-pinned its own after the ACS local stages (Repin the US spec-engine contracts after the ACS local SSI and immigration stages #1496), and its tree matches them, so these carry the gate calibration's move alone. The commit that re-pinned main's stale ones on c8984c4 (5ed3468) is dropped. On d4d66a9 the seed digests still recompute to the pins and the F0 report's
--checkpasses, so none of the three moves.The stake-scale floor moves the UK gate battery's mirrored digests and the spine certification part's. They are recomputed on main's
gates.json, which UK #1095 A/B fixes: the SPI age band (uk-data#628), households' HRAD from Table 8a receipts, and the SPI pay floor (uk-data#629) #1426 changed too.The UK declarations move source_stages.json, the coverage manifest and the H2 fixture.
#1426's nits
Vahid's third round on #1426 left two nits on
spi_benefit_coherence, and #1426 merged without them, so this PR carries them, in its last commit. Neither changes a build's data.private_pension_income, from PENSION). JSA Regs 1996 reg 81 takes every pound above £50 a week off contribution-based JSA, and ESA Regs 2008 reg 74 half of every pound above £85 a week off contributory ESA. B10 tests the copy's work alone, so the awards stand. The stage's receipt now shows how much rests on the omission: for each award, the support and weighted award of the copies whose pension payments are above its threshold, the most a reduction could take off. A figure is withheld where one or two source households carry it or the rest of the award. JSA's £50 is read from policyengine-uk'sgov.dwp.JSA.contrib.pension_disregard, with the other JSA limits. ESA's £85 is a constant, because policyengine-uk has no parameter for it. Reducing the awards would take them beyond B10's ruling, so the receipt measures that move first. The node now declaresprivate_pension_income, which moves the H2 fixture.Cost
A full spine build takes 21.7 minutes against 12.2. Each gate is now five gates, and the SPI tape forest's gates fit on 100,000 rows.
Tests
Run locally, targeted to the touched surfaces, and counted with JUnit, with no failures at the end and no skips but those named:
test_uk_qrf_gates, and H2 UK spine parity on the regenerated fixture (engine tier), 307;--maxfail=1, so at 5ed3468 each stopped at its first failure. A third stale fake, in the E6 identity receipt test, failed only here;ci_test_plan.py verify, the coverage manifest's--checkandspec_engine_coverage.py --check.Not run here, because they need the US engine:
engine_contract/us/test_spec_engine_coverage_tool.pyandengine_workflow/us/test_us_multispine_pool_tool.py.🤖 Generated with Claude Code