Retire the UK schema-3 gate path and keep household weights typed in-build (#654) - #691
Retire the UK schema-3 gate path and keep household weights typed in-build (#654)#691juaristi22 wants to merge 2 commits into
Conversation
…build (#654) Phase 1 of the #630/#654 build campaign, in three sections: - Typed-weights migration: uk_national_frame consumes and strips the household_weight input column (or takes explicit household_weights); validate_uk_national_frame refuses exported weight columns in carrier tables; all in-build readers use frame.weights_for("household"). The H5 boundary is unchanged - engine_tables materializes the column from the typed vector and the loader validates it on read. - Legacy schema-3 terminal gate path retired after a differential harness verified schema-4 battery parity on identical evidence (63 identical / 3 battery-richer / 0 contradictions / 0 legacy-better; receipt in the PR description). Removes uk_terminal_gate_report, write_uk_terminal_gate_report, UKReleaseParityEvidence, the parity_evidence build hook, and the orphaned threshold constants - build/uk/gates.json is now the single build-side policy source. - Rowwise clone entry narrowed to Frame | str | Path (one live caller, the rowwise driver); the duck-typed _dataset_tables fallback deleted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vahid-ahmadi
left a comment
There was a problem hiding this comment.
Net −1,821 lines with a differential receipt attached is the right way to retire a path, and "retirement was earned by verification, not a caller grep" is the sentence I'd want on every deletion PR of this size. Making the exported-weight-column absence structural — validate_uk_national_frame refusing the column rather than asserting agreement with it — is a better invariant than the one it replaces, and it's coherent with dropping the in-place household_weight refresh from the sampler that we went back and forth on in #656.
One row of the receipt is classified in a way I'd push back on.
missing_evidence / uk_weights_audit is a loosening on the default path, not a strengthening.
missing_evidence uk_weights_audit False evidence_absent battery_richer_stricter
Legacy returned False — a failure, which blocked every build. The battery returns evidence_absent, and on main that only becomes a blocking outcome when the candidate posture is on:
# gate_battery.py:499
elif outcome.status is GateStatus.EVIDENCE_ABSENT and release_candidate:--release-candidate defaults off (#662, delta 1). So on an ordinary build with no fit-weight records, legacy blocked and the battery does not. The PR body's parenthetical — "which still blocks shippability for release-blocking entries" — is true and is not the same claim: the report is non-shippable, but the build completes where it previously raised.
That matters here more than it would elsewhere, because this gate's own declared intent in uk/gates.json is:
Every fit-produced weight column carries a completed audit record. Armed by the SPI income stage; an absent audit is not a passing audit.
Under the battery on a default build, an absent audit now yields a build that isn't blocked — which is the outcome that sentence exists to forbid.
The other two rows in that bucket are genuinely richer: uk_input_mass_parity and uk_qrf_tail_concentration went omitted → evidence_absent, and legacy didn't block on those either, so naming them is strictly better. It's only False → evidence_absent that changes a blocking verdict.
To be clear about provenance: this behaviour changed in #662, when the battery became the executor — this PR only deletes the now-dead code. But the receipt is what's being offered as the warrant for deletion, and a table reading "contradiction: 0, legacy better: 0" doesn't quite carry the one case where legacy was stricter on the default path. Two options, either fine by me:
- Reclassify that row honestly (a fourth bucket, or a footnote on the existing one) so the receipt says what it found; or
- If an absent weights audit really should block every build per the spec note, make the entry block regardless of posture — either by treating
evidence_absentas blocking for that entry specifically, or by arming the gate so absence resolves tofailedrather than absent.
I'd lean to the second, since the spec note is unambiguous, but it's a bigger change and reclassifying is a legitimate answer if the intent has genuinely moved.
On the rest
- The typed-weights sweep looks complete from the diff — weighted-integrity, SPI support, CGT/HMRC calibration, HMRC restoration's mass mapper, diagnostics, sampling, and the driver's build-record total all moved to
frame.weights_for("household"). Keeping the table-levelspi_supportbuilders and the rowwise clone on explicit column parameters is the right line to draw: those are export-surface contracts, not carrier state. - Worth confirming the ordering at the loader seam still works now that the column is refused: the loader seeds typed weights from the stored column, so the strip has to happen before
validate_uk_national_framesees the frame. The diff reads that way to me, but it's the one place where "refuses the column" and "reads the column" meet. - Six scenarios × eleven gates with the full 66-row receipt inline is genuinely reviewable, which is unusual for a deletion this size. Whatever you decide on the row above, keep the receipt in the merge commit.
Vahid's review: the receipt row missing_evidence/uk_weights_audit was classified battery_richer_stricter, but legacy blocked every posture where the battery blocks candidates only - a loosening on the default path, dating to #662. Per the retirement rule (stricter-local behavior is ported, never reclassified away): - New closed-world manifest key evidence_absent_blocks: a gate entry declaring it blocks on absent evidence in every posture; the status stays honestly evidence_absent. Bool-only, refused on excused entries, serialized omit-when-false so it rides policy_sha256 and gates_manifest_sha256 exactly when armed (US manifest form untouched). - uk_weights_audit declares it, note names the provenance; the three UK vintage digests move, mirrored in contract.py per the lockstep. - Receipt reclassified: legacy_stricter_ported=1 with a footnote saying what legacy did and where the strictness went. - Tests: generic flag mechanism, UK default-posture blocking set is exactly {uk_weights_audit}, manifest pin that no other entry opts out; seam registry binds a toy audit; the real-battery staging test supplies genuine stage audit evidence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You're right, and taking the second option — the spec note is unambiguous and our own retirement rule said stricter-local behavior gets ported, not reclassified away. Landed in the review round: The port. New manifest vocabulary: a gate entry may declare The receipt. Reclassified rather than left flattering: the row is now its own bucket — — with a footnote recording that legacy blocked every posture where the battery blocked candidates only, and that the strictness was ported here rather than reclassified away. PR body table updated to match; the receipt stays in the merge commit. You're also right on provenance: the loosening dates to #662 when the battery became executor — this round restores the pre-#662 enforcement while keeping the schema-4 taxonomy. Tests. Three additions: the generic mechanism (flagged entry blocks a default-posture build, unflagged doesn't, and the armed flag moves both digests), the UK regression ( On the loader-seam ordering: confirmed — 🤖 Generated with Claude Code |
Closes #654. Phase 1 of the #630/#654 build campaign under the #665 master epic: the retire-and-clean stack that lands before the credentialed measurement runs. Three sections, one coherent motion — every seam the Frame migration superseded is retired, not flagged.
1. Typed household weights are the only in-build weight state (#654 tail 1, expanded)
uk_national_framenow consumes and strips thehousehold_weightinput column (or takes an explicithousehold_weightsargument), andvalidate_uk_national_framerefuses exported weight columns in carrier tables instead of asserting column/vector agreement — the invariant is structural, not conventional. Every in-build reader moved toframe.weights_for("household"): the weighted-integrity evidence path, SPI support, CGT/HMRC calibration, HMRC restoration's distributional-mass mapper, diagnostics (now takes a Frame — the carrier invariant), sampling, and the driver's build-record total.The H5 boundary is unchanged:
engine_tablesmaterializes the column from the typed vector at every export, and the loader still seeds typed weights from the stored column and validates on read. Table-level APIs (spi_supportbuilders, the tables-level rowwise clone) keep their explicit column parameters — those are export-surface contracts, not carrier state.2. Legacy schema-3 terminal gate path retired, with a differential receipt
Retirement was earned by verification, not a caller grep: a differential harness drove identical evidence through both paths — the legacy
uk_terminal_gate_reporttrio and the schema-4GateBatteryRun— across six scenarios (clean pass, weight-ratio breach, ESS-floor breach, degenerate column, missing evidence, malformed evidence), 66 case×gate comparisons:uk_input_mass_parity+uk_qrf_tail_concentration(legacy silently omitted un-armed gates; the battery names them)uk_weights_audit— legacy blocked every posture, the battery blocked candidates only. Per the retirement rule (stricter-local behavior is ported, never reclassified away), the entry now declaresevidence_absent_blocksingates.jsonand the battery blocks its absence in every posture, status staying honestlyevidence_absent. The UK vintage digests move with it (mirrored incontract.py).Full differential receipt (66 rows)
With the diff clean, the retirement removes
uk_terminal_gate_report,write_uk_terminal_gate_report,UKReleaseParityEvidence, theparity_evidencebuild hook, the schema-3 signing/attestation block, and the orphanedUK_MAX_TO_MEDIAN_WEIGHT_RATIO/UK_MIN_ESS_FRACTIONconstants (~930 lines fromterminal_gates.py).build/uk/gates.jsonis now the single build-side policy source — the #630 weight-ratio re-baseline becomes a two-place edit (manifest + the deliberate data-shard mirror incontract.py, untouched here so the published June release keeps verifying). Legacy-only tests retired with their subject; battery assertions were converted to battery-only regression tests.3. Rowwise clone entry narrowed (#654 tail 2)
Adjudication recorded per the charter:
clone_uk_dataset_with_rowwise_geographyhas one live non-test caller (tools/build_uk_rowwise_dataset.py), so the route is narrowed toFrame | str | Path(ladder-pattern refusal), not retired. The duck-typed_dataset_tablesfallback is deleted — both public entries now only ever pass Frame-or-path.Not in this PR
microcosm-datacontract mirrors andgates.jsonnumeric parameters — those move with the National staging rebuild fails the current terminal gates: constant source_year release column and SPI weight tail above the reviewed maximum #630 receipted values in a later PR.cgt_calibration.py/cgt_imputation.pyare staged-for-future by named issues and were adapted, not deleted.Verification
uv run --no-sync pytest -q): green.uv run --no-sync ruff check .): clean.Refs #630, #611, #612, #618, #635, #665.
🤖 Generated with Claude Code