Repository navigation
Conversation
Port the ITL differential check from the closed pilot PR #1413 (head 58411eb) onto main as a standing check of any UK national build. It sets a calibrated file's Income Tax payers beside HMRC's July 2026 projections at the calendar-2025 window: Table 2.5's bands and all-band count, Table 2.1's United Kingdom counts at State Pension age and 65 and over, Table 2.2's twelve region-tier areas and their residents' sum, and the count below State Pension age. It is a check, not a target. The dated SPI path (--dated-year, dated_people, SINGLE_TAX_YEAR) is dropped: the spi_2023_* measures it read never landed. The rows are main's vendored hmrc_itl_taxpayer_counts.json, which #1414 extended with Tables 2.1 and 2.2. Since #1414 binds Table 2.2 (hmrc_itl_age), the in-sample flags read the targets by the names its references and a run's diagnostics give them. New tests hold BOUND_BY to the contract's target ids and references, and show the contract and a run's diagnostics marking the areas in sample. A sum stays in sample only when its parts are and outside_parts is zero. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A differential test: every bound Table 2.5 band and Table 2.2 area cell expects exactly the value the build's Ledger compile gives its target at 2025, as the committed parity receipt records it, and the checked names are exactly the contract's references for the families BOUND_BY reads. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-house Opus 5.5 review approved with minors and nits; this fixes them. - Without --diagnostics the in-sample basis is this tree's active target references at --year (target_references.json), not the contract's target ids, so a year the references do not carry binds nothing. --references replaces --contract and excludes --diagnostics. - With --diagnostics each bound cell carries the run's final_estimate of its target beside the model count read here (model_vs_run_estimate), a model-side differential; rows bound only for want of a loss weight are counted in the in-sample basis. - Cells name their target reference (ITLCell.reference), shared with the bound-target match. - The unused weights parameter is gone. - JSONL rows skip whitespace-only lines. - New tests pin the bool(parts) and len(parts) guards, the mask edges (band edges, 65, the taxpayer and State Pension age flags, the tier codes), the year of the reference basis, the JSONL branch, unweighted rows and the mutually exclusive bases. The family invariant is now recomputed from the cells, and a property test shows no residual can equal a tolerance, so the verdict never turns on < against <=. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPT-6.1 Sol review found that a bound cell the run estimated at zero got no relative difference and fell out of the run-estimate summary, so a model count of 10 beside a zero estimate reported no comparison at all. Each estimated cell now also carries its count difference, and the summary counts every estimated cell, names the zero estimates and reports the largest count difference beside the largest relative one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis
marked this pull request as ready for review
October 28, 2026 17:32
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Review: an in-session Opus 5.5 review approved round 1 with minors and nits, all fixed at 1d4ae4c. A GPT-6.1 Sol review approved at 1d4ae4c with one minor (a run estimate of zero dropped out of the model-side comparison), fixed in the fourth commit (49633e3), which a Sol delta review approved with no new findings.
What this adds
tools/check_uk_itl_projections.pyandmicrocosm.build.uk_runtime.itl_differentialare a check to run on any calibrated UK file. They compare it with HMRC's Income Tax liabilities statistics (July 2026) projections at the calendar-2025 window (3/12 of 2024-25 plus 9/12 of 2025-26). It is a check, not a target: nothing here feeds calibration or refuses a build, and nothing runs it automatically.Each cell reports its model count, its relative miss and a tolerance fixed before any file is read: 5% for national cells and 10% for regional ones. Each cell also carries an in-sample flag. With
--diagnostics, each bound cell also shows the run's ownfinal_estimateof its target beside the model count read here, as a count difference (model_minus_run_estimate) and, when the estimate is above zero, a relative one (model_vs_run_estimate). The summary counts every estimated cell, those estimated at zero among them (zero_estimates), and reports the largest relative and count differences. That is a model-side differential: a gap means the check and the solve read the cell differently, or the file's weights are not the solve's.--strictexits 1 on any miss.Ported from #1413, with these changes
The code comes from the closed pilot PR #1413 (head 58411eb). That pilot lost its A/B and closed unmerged; the methodology call is d2111. Its round-3 in-sample logic, from the GPT-6.1 Sol review, is kept unchanged. Three things changed:
--dated-year,dated_peopleandSINGLE_TAX_YEARread thespi_2023_*measures, which never landed on main.hmrc_itl_taxpayer_counts.json. Bind HMRC's pensioner taxpayers by area and re-index the SPI State Pension bands (#1069) #1414 added Tables 2.1 and 2.2 to it. The engine test'sMICROCOSM_UK_ITL_DIFFERENTIAL_ROWSoverride, a stopgap until those tables were vendored, is dropped. The CLI's--itl-rowsstill swaps in another feed.hmrc_itl_age:hmrc.itl.state_pension_age_taxpayersandhmrc.itl.taxpayers_aged_65_and_overin the twelve areas). The new tests check that the cells read those targets by the names the contract's references and a run's diagnostics use:<target>@<GSS code>for areas and<target>.band_<lower>_<upper>for bands. Each cell exposes its name asITLCell.reference.--year. It readstarget_references.json; the pilot read the contract's target ids. A year the references don't carry now binds nothing: the pilot marked all 41 cells in sample at 2024 and 2026.--referencesreplaces--contract, and the two bases are mutually exclusive.weightsparameter is gone. JSONL rows skip whitespace-only lines.How a cell gets its in-sample flag
--diagnostics, the basis is the run'scalibration_diagnostics.json. A row binds when its weight is above zero, or when it carries no weight; the basis string counts the rows bound that way.--year.outside_partsis zero.outside_partsis the weight of people the cell counts that no part counts, for example a taxpayer below the first band edge or one in no area.Worked example (aggregates only)
These were computed locally from files on disk, and none of them is a new national build.
runs/pensioner-1069/candidate/calibration_diagnostics.json) mark all 41 cells in sample before the file is read;outside_partsthen decides the sums. The pre-Bind HMRC's pensioner taxpayers by area and re-index the SPI State Pension bands (#1069) #1414 baseline's diagnostics mark only Table 2.5's 12 cells.test_the_bound_cells_expect_what_the_build_compilesruns the same comparison in CI against the committed parity receipt.runs/spi-dated-bands/measures/itl_{baseline,pilot}.json) came from the same 256 rows. On main 7d45c0c they show 41 cells and 0 misses; the largest relative difference, +6.44% in the North East's State Pension age cell, is inside its 10% tolerance. On the pilot they show 3 misses.budget-day-2026-10-28reservation.Invariants (property-tested)
test_the_window_weights_the_two_tax_years(Hypothesis)test_the_cells_do_not_depend_on_the_order_of_the_rows,test_model_counts_do_not_depend_on_the_order_of_the_people(Hypothesis),test_the_report_is_deterministictest_the_bands_and_the_areas_partition_their_totals(Hypothesis)test_a_sum_in_sample_is_fixed_by_bound_parts(Hypothesis),test_a_sum_is_in_sample_only_when_its_parts_count_everyone_it_counts,test_a_run_that_binds_table_2_2_marks_its_cells_in_sample,test_without_table_2_2_the_united_kingdom_counts_have_no_parts,test_a_sum_given_without_one_of_its_parts_is_out_of_sample--strictexits 1 exactly on a misstest_the_verdict_is_the_tolerance(Hypothesis),test_strict_exits_one_exactly_on_a_miss<against<=: every residual is a multiple of 2⁻⁵³, and 0.05 and 0.10 aren'ttest_no_residual_lands_on_a_tolerance(Hypothesis)test_the_bound_cells_expect_what_the_build_compilestest_every_estimated_cell_counts_in_the_run_summary(Hypothesis),test_a_zero_run_estimate_still_shows_its_difference[lower, upper)bands, age ≥ 65, taxpayer,is_SP_age, tier codes onlytest_the_cells_read_the_bindings_at_their_edgestest_bound_by_names_the_contract_targets_and_their_referenceschecks thatBOUND_BYnames contract targets of familieshmrc_itlandhmrc_itl_age, and that their references are named as the cells read them.Each of these mutations turns the tests red:
BOUND_BYtarget;@;outside_parts;bool(parts)guard;len(parts)guard;age > 65;<=against<in the verdict is an equivalent mutant (see the table above).Tests and CI
packages/microcosm-build/tests/engine_free/uk/test_uk_itl_differential.pyhas 43 tests, all passing locally (-n 4). The pilot's 30 tests also passed unchanged on main before the port's edits.packages/microcosm-build/tests/engine/uk/test_uk_itl_differential_build.pyis marked slow and skips unlessMICROCOSM_UK_ITL_DIFFERENTIAL_H5names a built national file.tools/ci_test_plan.py verifypasses.list-job engine-freelists the first file andlist-job engine-ukthe second, since ownership comes from the directory.test_uk_solve_scope,test_transport_graph,test_transport_composed_graph,test_spec_only_country_packagesandtest_frame_serializer_registry.test_uk_solve_scopewas rerun at the head (126 tests).axiom: n/a: diagnostics tool, no policy rule
🤖 Generated with Claude Code