Skip to content

UK parity increment 4: weighted integrity gates (input-mass parity + QRF tail concentration) in the terminal battery (#609) - #610

Merged
MaxGhenis merged 17 commits into
mainfrom
uk-parity-inc4-weighted-integrity-gates
Aug 6, 2026
Merged

UK parity increment 4: weighted integrity gates (input-mass parity + QRF tail concentration) in the terminal battery (#609)#610
MaxGhenis merged 17 commits into
mainfrom
uk-parity-inc4-weighted-integrity-gates

Conversation

@juaristi22

@juaristi22 juaristi22 commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Closes #609. Increment 4 of the #578 parity plan: the UK terminal battery gains the two US integrity gates that were each purchased with a named incident — input-mass parity (#278) and QRF tail concentration (#462) — under the attested, batched, fail-closed report from increment 1 (#588).

What landed (deliverables 1–5 of #609)

Shared helper promoted, not forked (deliverable 1). us_input_mass_totals moved to populace.build.input_mass.input_mass_totals; the us_runtime path re-exports it, so US call sites are byte-identical. A UK analog uk_dataset_input_mass_totals handles the national person/benunit/household table layout (household weights broadcast through membership; names namespaced entity.column per the degenerate-gate convention, since national tables don't enforce globally unique column names).

uk_input_mass_parity_gate (deliverable 1). Wraps the shared gate verbatim — zero candidate mass fails at any tolerance, candidate-only columns are reported and never fail, near-zero reference columns are skipped, unused entries are reported — and adds the universal exclusion discipline the shared gate lacks: a stale exclusion (column now within tolerance) fails, via a per-column probe that reuses the shared gate's exact semantics; below-floor and absent-from-reference entries are dormant and reported. The frozen reference's filename, revision, sha256, and vintage are required by UKInputMassReference, recorded in the gate details, and bound into the evidence digest.

uk_qrf_tail_concentration_gate (deliverable 2). Surface derived from the fit_weighted_qrf_stage* operations' declared outputs in hmrc_income_source_stages.json via a new uk_hmrc_weighted_qrf_output_columns() in the source-contract module (47 columns today; a new manifest-declared QRF output is covered the day it's declared). Per the issue, no sparsity filter: every declared, present, numeric output is checked; min_nonzero_records is the sole thinness guard. Stale/dormant discipline comes from the shared gate.

Wiring (deliverable 3). Both gates follow the increment-1 optional-evidence pattern: a path that supplies no reference/thresholds omits them (never a placeholder pass), and an armed gate missing either fails closed with a named line. Policies are sealed into policy_sha256 (_terminal_gate_policy_payload gained input_mass_parity / qrf_tail_concentration slots); two new evidence_sha256 families follow the existing precedents (input-mass hashes the frozen reference identity+totals; QRF tail projects the gate's recorded observables, like release_dataset does). Report schema 2→3, attestation 4→5, membership validation extended. Evidence artifacts still land before the batched raise — a red run leaves the signed diagnosis on disk (verified). No second raise added.

Registers (deliverable 4). Committed empty registers (uk/input_mass_reviewed_exclusions.json, uk/qrf_tail_reviewed_exclusions.json) declared in the country package, loaded by default by the new --input-mass-* / --qrf-tail-* flags on tools/build_uk_national_dataset.py (complete-arming enforced: partial threshold sets are a CLI error). Mandatory non-empty reason, dormant reported, stale fails.

Mutation tests (deliverable 5). A zeroed column, a 99.9%-loss column, and a concentrated column each fail by name through the armed battery; a crash inside either evaluator appends a named failure without masking pending gates (#547); dropping an evidenced gate fails attested-membership validation and a committed membership test; stale exclusions fail in both gates; thresholds provably move policy_sha256; legacy no-evidence paths keep the exact five-gate membership.

Publication contract in lockstep. populace-data/contract.py pins moved with the producer (schema 3, attestation 5, new policy digest, evidence-stage→gate map, detail schemas, stale-exclusion and reference-identity observables). The pinned policy digest is the certified default policy, in which the increment-4 slots are unarmed — arming a published release requires a reviewed pin move alongside the committed threshold constants, which is exactly the "a threshold outside the policy hash is not attested" rule.

Deliverable 0 (measurement pass) — tooling shipped, numbers deferred

No licensed UK artifact exists on this machine, so the measurement pass itself could not run here. What shipped so it can run anywhere with gated data:

  • tools/measure_uk_weighted_integrity_baselines.py — weighted per-column totals plus top-{10,100,500,1000} concentration and carrier counts for any national H5 (certified compact, staging), including a ready-to-fill reference-JSON template.
  • tools/build_uk_efrs_parity_reference.py --emit-weighted-totals PATH — the pinned, sha-verified eFRS incumbent's totals in the exact schema the gate consumes. It refuses to write inside the repository.

Licensing: resolved, and the recorder enforces it

The EUL question is answered from the licence text rather than left open. End User Licence CD137 v16.00 (25 February 2026) clause 8 requires "adhere[nce] to the statistical disclosure control standards as detailed in the Research data handling and security guide for users in any outputs I produce and publish" — publishing outputs is permitted, conditioned on SDC. Clause 4's bar on sharing "any Dataset(s) derived from the Data Collection(s)" governs record-level derived data (clause 13's deposit duty reads the same way), not summary statistics. Clauses 11–12 require acknowledgement and citation in any publication.

CD171-ResearchDataHandling v17.00 §5.2.1 supplies the rules: minimum threshold of 3 (10 advised against secondary disclosure, 30 for some studies), and "any output that refers to unit records, e.g. a maximum or minimum value, must be avoided".

So population-level weighted per-column totals are publishable with citation. But the first version of the recorder emitted a per-column max_abs_value and would have reported a top-k share for a column with a handful of carriers — and tail concentration is the (n, k) dominance statistic, so at small k the share times the total approximates the mean of those few records. Fixed in fdbcbb0: no maxima or minima, thin columns suppressed below --sdc-minimum-count (default 10), --top-k narrower than that count refused, and the applied rules plus citation obligations recorded in the payload. A genuinely concentrated column above the threshold stays fully visible, so the #462 signature remains detectable.

Still the caller's to confirm: the SPI PUT (SN 9422) carries its own SPI usage agreement as a Special Condition under clause 3, with HMRC's statutory taxes-confidentiality duties behind it. Raise --sdc-minimum-count to 30 if it or the FRS terms require it.

Measurement pass: run, and it is why the gates ship unarmed

The pass ran against the pinned incumbent (enhanced_frs_2023_24.h5, sha 584ae33d… verified before reading) and the certified compact (populace_uk_2023.h5, sha f17306cc… verified). Four results, all recorded beside the policy dataclasses so no later reader reintroduces the US constants:

  1. The US 0.75 top-share threshold is wrong for this surface. At the US top_k=100 / min_nonzero_records=500 settings the incumbent exceeds 0.75 on 16 of 28 checked columns. UK reported-benefit columns sit on small, high-intensity subpopulations, so concentrated weighted mass is their normal state.
  2. No single global top-share threshold is both incumbent-compatible and incident-catching. The lowest no-headroom value on a broad surface is ~0.996, which would not have caught Build M live default: non_sch_d_capital_gains lands at $74.6B vs its $10.2B SOI target (+635%); Schedule-D CGD route is empty #462 itself (89% of mass in 100 of 2,295 carriers). Tightening to a discriminating ~0.54 requires min_nonzero_records=5000, dropping 17 of 31 columns.
  3. The mass floor is adjudicated at 0.0. The US 1e9 would stop checking 50 of 131 columns, including person.gift_aid (£996M) and person.charitable_investment_gifts — the two release_input_coverage_manifest.json names as distributional-required. The surface mixes units (currency into the trillions, weighted person counts, flag counts), so a magnitude floor is not a coherent materiality filter; 0.0 still skips the three exact-zero reference columns because the shared gate compares <=.
  4. The certified compact is not a valid candidate against this reference, confirming the issue's reading of option 2. It carries 8.1% less household mass (28,840,551 vs 31,389,678), a different surface (150 columns vs 131, 22 candidate-only), and per-column drift with a 22.3% median and 578% maximum — a differently calibrated build, not a mass-preserving derivative. It also zeroes gift_aid and charitable_investment_gifts and lacks all eleven hmrc_spi_* columns, because it is the input to the HMRC/SPI stage that creates them. Comparing the two reproduces the Export input-mass-parity gate references the raw base, flagging correct calibration gains on PUF-imputed columns #327 failure mode on the restoration surface.

Fifth result — the production release pair (added after review questions). The comparison production actually runs is the shipped release against the next shipping of the same kind — the _export_input_mass_gate pairing whose reference #327 adjudicated to a certified release. Measured on the only consecutive certified UK pair (72aeefc 2026-06-11 → dd68c73 2026-06-19, both sha-verified): zero columns dropped, zero zeroed, four added — the #278 incident class the gate exists for passes cleanly on a real shipping with no threshold judgment. Per-column drift, however, ran median 10.52% with a +2,973.71% maximum (person.adult_ema, intentional reported-benefit repopulation): any tolerance between 5% and 50% would have failed 22–89 columns of a reviewed shipping. The no-headroom release-arm boundary is therefore the pair's exact maximum, relative_tolerance = 29.737060 with minimum_reference_total = 0.0, ratcheting down (never up) as consecutive same-code shippings stabilise, re-measured per certified pair. Adjudicated decision: the constant is deliberately not committed here — it lands with the release-arm call-site PR (items 6/8), because a threshold without the call site that exercises it cannot be attested in a report; the derivation lives in the weighted_integrity docstring so that PR arms from evidence.

So both thresholds require a staged candidate, and the gates correctly remain evidence-gated and unarmed. The floor's derivation is recorded but not committed as a constant, because a policy arms both thresholds together; both land in the follow-up with the two lockstep digest moves.

Acknowledgement: figures derive from the enhanced Family Resources Survey artifact pinned at revision 655dd07e…, itself derived from the DWP Family Resources Survey distributed by the UK Data Service, and from the certified PolicyEngine UK compact. Per EUL clauses 11–12 the citation form specified in the study metadata applies to any onward publication.

Still open (user-owned)

  1. Wire the release-arm call site (shipped release vs next shipping) when the items-6/8 lanes land, arming input-mass from the measured pair boundary above; the staging-seam arm still needs a staged candidate: tools/build_uk_national_dataset.py needs the licensed UKDS SPI donor put2223uk.tab (SN 9422), the HMRC ODS surface, and a raw FRS directory. That is the only remaining blocker on arming either gate.
  2. Threshold-commit follow-up: constants at the measured boundary with derivations next to UK_MAX_TO_MEDIAN_WEIGHT_RATIO, plus terminal_gates.UK_TERMINAL_GATE_POLICY_SHA256 and contract._UK_TERMINAL_GATE_POLICY_SHA256 moved together.
  3. Confirm study Special Conditions (SPI usage agreement; FRS terms) before posting measurement numbers anywhere public.
  4. Unrelated bug worth its own issue: registry.py:103 points populace.data.download("uk", 2023) at policyengine/populace-uk-private, which 404s for a fine-grained token without explicit policyengine org scope — the documented UK download path fails confusingly for org members whose token omits it.
  5. Second call site (calibrated candidate vs staging, mirroring _export_input_mass_gate): the UK dense/local epic: rowwise constituency-grain dataset by cloning the national staging pool (US dense-arm analog) #495 rowwise arm re-gates household weights only; both helpers are importable from populace.build.uk_runtime and wire in when that arm carries full entity tables (items 6/8 territory).

Verification

  • uv run pytest — full workspace suite green (includes 16 new weighted-integrity unit tests and 9 new battery tests).
  • uv run ruff check . — clean.
  • Red-path smoke: armed gate on a zeroed column → report written with signed attestation, named failure, and reference identity before the raise.

🤖 Generated with Claude Code

juaristi22 and others added 4 commits August 4, 2026 11:44
…ry (#609)

Port the two incident-purchased US integrity gates to the UK per #609
(increment 4 of the #578 parity plan):

- Promote us_input_mass_totals to shared populace.build.input_mass (US
  import path re-exports; no fork).
- New uk_runtime.weighted_integrity: uk_dataset_input_mass_totals over the
  national tables, uk_input_mass_parity_gate (verbatim #278 semantics plus
  the universal stale-fails exclusion discipline the shared gate lacks,
  with the frozen reference identity recorded), and
  uk_qrf_tail_concentration_gate whose surface derives from the
  fit_weighted_qrf_stage* outputs declared in the HMRC source manifest —
  no sparsity filter, min_nonzero_records is the sole thinness guard.
- Both gates join uk_terminal_gate_report evidence-gated: unarmed paths
  omit them; an armed gate missing its reference or measured thresholds
  fails closed by name. Thresholds have no committed defaults (the #609
  measurement pass owes them) and are sealed into policy_sha256 with new
  input_mass_parity / qrf_tail_concentration evidence digests. Report
  schema 3, attestation 5; populace-data publication contract moved in
  lockstep, including detail-schema and stale-exclusion observables.
- Committed empty reviewed-exclusion registers, --input-mass-* /
  --qrf-tail-* staging launcher flags, a measurement tool
  (measure_uk_weighted_integrity_baselines.py), and an
  --emit-weighted-totals extraction for the pinned eFRS incumbent that
  refuses to write inside the repo pending the UKDS EUL confirmation.
- Mutation tests to the increment-1 standard: zeroed, 99.9%-loss, and
  concentrated columns each fail by name through the armed battery; a
  crash inside either evaluator appends a named failure without masking
  pending gates; dropping an evidenced gate fails membership validation.

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

The measurement recorder exists so its numbers can be posted on #578, which
makes its output a publication under UK Data Service End User Licence CD137
v16.00 clause 8: "To adhere to the statistical disclosure control standards
as detailed in the Research data handling and security guide for users in any
outputs I produce and publish." CD171-ResearchDataHandling §5.2.1 sets those
standards for Safeguarded data — never report cells based on one or two cases
(minimum threshold of 3, with 10 advised where several outputs share a
source), and "any output that refers to unit records, e.g. a maximum or
minimum value, must be avoided".

The recorder violated both in spirit: it emitted a per-column max_abs_value,
and it would have reported a top-k concentration share for a column with a
handful of carriers. Tail concentration is the (n, k) dominance statistic, so
at small k the share times the total approximates the mean of those few
records. Now: no maximum or minimum is emitted at all; a column with fewer
carriers than --sdc-minimum-count (default 10) reports no shares, no carrier
count, and no nonzero share; a --top-k narrower than that count is refused;
and the payload records the rules applied plus the clause 11-12 citation
obligations. Per-column weighted totals aggregate every carrier and remain
unconditional. A concentrated column above the threshold is still fully
visible, so the #462 signature the gate exists to catch is undimmed.

Separately, uk_dataset_input_mass_totals resolved a benunit's weight through
whichever member person came first, silently assuming benunits nest inside a
single household. The effective-mass coverage gate enforces that invariant,
but it is a different gate in a batched report, so this path now validates it
and fails closed by name instead of totalling a split benunit against one of
its households.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
--emit-weighted-totals ran after the reference writer, whose --output
defaults to the committed efrs_parity_reference.json. So measuring the
incumbent also regenerated the coverage contract against whatever
policyengine-uk happened to be installed — silently, as a side effect. With
policyengine-uk 2.89.4 locally against the committed 2.89.0 (871 engine
variables vs the recorded 866) that is a live diff, and the operator running
the #609 measurement pass has no reason to expect a contract rewrite.

--emit-weighted-totals is now its own mode: it writes the gate input outside
the repository and exits, leaving the committed reference untouched.
Regenerating the contract stays a separate, deliberate run.

With policyengine-uk installed the extraction is testable for the first time,
so it now has one: a synthetic three-table H5 with the incumbent's shape and
the sha pin stubbed proves the weight broadcast (distinct per-household
weights, so a broadcast error cannot pass), the exclusion of structural and
non-engine columns, the pinned identity block, that the emitted file loads
straight into uk_input_mass_parity_gate as a reference, and that a totals run
leaves the committed reference byte-identical. That path was previously
unexercised in the workspace and was about to be run against licensed data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…609)

Ran the reference extraction against the pinned enhanced-FRS incumbent
(sha 584ae33d…, verified before reading) and measured its own concentration
baseline. The findings are written into the module docstring because they are
the reason the gates ship unarmed, and a later reader with the US constants
in hand would otherwise reintroduce them:

- The US 0.75 top-share threshold fails the incumbent on 16 of 28 checked
  columns at the US top_k/min_nonzero settings. UK reported-benefit columns
  sit on small, high-intensity subpopulations, so concentrated weighted mass
  is their normal state.
- No single global top-share threshold is both incumbent-compatible and
  incident-catching: the lowest no-headroom value on a broad surface is
  ~0.996, which would have missed the #462 incident itself (89% of mass in
  100 of 2,295 carriers).
- The incumbent cannot supply a QRF baseline at all. Eleven stage-1 outputs
  and five stage-2 disability columns are absent from it, and the seven
  stage-1 columns it carries hold survey-reported FRS values rather than
  donor draws, so that is a different distribution from the one the gate
  polices. The QRF threshold needs a staged candidate.
- A magnitude floor is not a coherent materiality filter on this surface,
  which mixes currency totals in the trillions with weighted person counts
  and flag counts. The US 1e9 floor would stop checking 50 of 131 columns,
  including gift_aid and charitable_investment_gifts — the two the release
  input coverage manifest requires distributional effective mass for. A 0.0
  floor still skips the three exact-zero reference columns, since the shared
  gate compares <=.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 and others added 3 commits August 4, 2026 16:32
The certified compact is not a valid candidate against the pinned incumbent,
which confirms the issue's reading of reference option 2. Measured: 8.1% less
household mass (28,840,551 against 31,389,678), 150 columns against the
reference's 131 with 22 candidate-only, and per-column drift with a 22.3%
median and a 578% maximum — a differently calibrated build rather than a
mass-preserving derivative. It also zeroes gift_aid and
charitable_investment_gifts and carries none of the eleven hmrc_spi_* columns,
because it is the input to the HMRC/SPI stage that creates them: those are
stage-1 QRF outputs. Comparing the two reproduces the #327 failure mode on the
restoration surface.

So both thresholds need a staged candidate, and the adjudicated 0.0 mass floor
stays uncommitted as a constant because a policy arms both together.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…609)

The comparison production actually runs is shipped-release against next
shipping of the same kind — the _export_input_mass_gate pairing whose
reference #327 adjudicated to a certified release. The only consecutive
certified UK pair (72aeefc 2026-06-11 -> dd68c73 2026-06-19, both
sha-verified) measures it:

- The #278 incident class passed cleanly: zero columns dropped, zero zeroed,
  four added (reported, never failing). The gate's core protection needs no
  threshold judgment and would not have false-alarmed on a real shipping.
- A drift tolerance cannot be tight yet: 143 shared nonzero columns moved
  with median |drift| 10.52% and max +2,973.71% (person.adult_ema),
  dominated by intentional reported-benefit repopulation. Tolerances between
  5% and 50% would have failed 22-89 columns of a reviewed shipping. The
  no-headroom release-arm boundary is the pair's exact maximum, 29.737060,
  ratcheting down as consecutive same-code shippings stabilise — never up.

Recorded in the module docstring next to the other measurement findings so
the release-arm wiring (items 6/8) arms from evidence, not from the US 0.5.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…al tests

Review asked whether the UK port reinvented anything. The audit: gates.py and
the US release tool are byte-identical to main, both UK wrappers call the
shared gate functions, and us_input_mass_totals is the same function object
as the promoted helper. Three differential tests now lock that equivalence
against future drift:

- Equivalent records expressed as a populace Frame (the US path) and as UK
  national tables produce identical per-column weighted totals, pinned once
  to a hand computation so both paths anchor to the semantics, not merely to
  each other.
- Without exclusions, uk_input_mass_parity_gate reproduces the shared gate's
  verdict, failure lines, and details verbatim; the UK result may only add
  reference_identity and the stale/dormant register fields #609 requires.
- With exclusions, uk_qrf_tail_concentration_gate matches the shared tail
  gate's verdict, failures, and details exactly, differing only in the UK
  battery gate name.

The deliberate deltas stay what the issue mandated: stale-exclusion failures
on the input-mass side (the shared gate skips excluded columns) and no
sparsity filter on the QRF surface.

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

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorough adversarial pass (same battery every populace PR gets this week). The core of this PR is right — the shared-helper promotion keeps US call sites byte-identical, the UK household-weight broadcast checks out against hand computation at fixture scale, and the differential tests genuinely pin the UK wrappers to the shared gate semantics. The blockers are all in one family, and it's the same family we've been curing across the repo this week (see the #616 review trail): places where the gate's authority over its own inputs is incomplete, so absence or malformation slips through as success.

1. Legacy output path bypasses the fail-closed HMAC contract (pre-existing, but it now swallows your new attestations too). _AttestedUKTerminalGateReport inherits a .passed that checks only functional gates (gates.py:143); with signing credentials unset, the serialized payload correctly fails — but the legacy --input-coverage-json branch skips the raising writer, trusts the object property, and proceeds to staging output (national_build.py:443). Repro on the green four-row fixture with the key unset: report.passed == True while report.report_payload()["passed"] == False with a live RuntimeError in _signing_error.

2. A declared-but-absent QRF output produces a passing zero-column gate. Absent/nonnumeric declared outputs are recorded but omitted from the maps handed to the shared gate (weighted_integrity.py:638, :667), and publication checks only stale exclusions (contract.py:1322). Repro: passed=True, columns_checked=0, absent_columns=['declared_but_absent']. Declared-but-missing should be a named failure — this is exactly the "family silently vanishes" class that run 7 just demonstrated at production scale on the US side.

3. The receipted/expiring exclusion contract from the PR body isn't in the schema. It's column -> reason only (weighted_integrity.py:155) — no approval identity, no expiry, so an exclusion is immortal. 4. Two adjacent leaks: a present column thinner than min_nonzero_records exits before exclusion classification (gates.py:2459 — its exclusion appears in no collection: repro thin={'person.x': 4}, passed=True, reviewed={}, stale=[], dormant=[]), and the plain json.loads + str() loader accepts duplicate keys, nulls, and non-string names.

Suggested path: #616's fix rounds are landing a canonical-authority pattern (module-level plan/surface authorities, live-content digests receipted into gate manifests, no caller substitution) that solves 2–4 as a class — happy to converge on that machinery rather than have you build a parallel one; it should be on main within a day. Finding 1 is a scoped fix in the legacy branch (route it through the raising writer). Everything else here is merge-ready once those close.

MaxGhenis added a commit that referenced this pull request Aug 5, 2026
…reporting, per-country gate selection (#611) (#619)

* Country-agnostic gate battery core: phased evaluation, unconditional reporting, per-country selection (#611)

The tooling half of #611 increment 1. populace.build.gate_battery
evaluates a country's declared gate selection phase by phase, resolves
every declared entry to exactly one of five statuses (passed / failed /
not_applicable / evidence_absent / unreached), persists the full
attested report before any blocking decision, and hashes every declared
threshold into the policy digest. gates.json gains a per-country phases
order and per-entry phase; thresholds live in entry parameters; a
reviewed not_applicable reason turns a deliberately unselected gate
into a reported receipt. The Belgian spec migrates mechanically (nine
entries, all terminal) and a compatibility test proves it runs as
declared, with unimplemented gate names surfacing as named
evidence_absent gaps rather than crashes. The UK terminal battery is
untouched; its migration onto this executor is the consumer half,
sequenced after #610 and the #612 Frame carrier.

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

* Derive gate battery provenance from the evaluated manifest

* Freeze gate parameters before battery evaluation

* Format gate battery changes and extend changelog

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Max Ghenis <mghenis@gmail.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor

Pushed the review remediation on your branch (María — your gate design and naming are intact; ten fix commits): the legacy --input-coverage-json branch now routes through the raising writer so missing signing credentials fail closed on every output path; declared-but-absent QRF outputs fail by name (zero-checked-columns can no longer pass); the exclusion loader strictly rejects malformed shapes; thin-column exclusions always classify; input-mass evidence is anchored to the reviewed enhanced-FRS identity at load, gate, and publication; and the differential US-parity tests plus the review repros are committed regressions. One thing deliberately left for you: the receipted/expiring exclusion schema design (approval identity, expiry semantics) — marked with TODO(PR #610 review) and noted in the changelog, since that's a policy choice about your gate rather than a bug. Full workspace 5,166 passed / 0 failed; UK battery 706; #583 guard 495 exact.

@MaxGhenis MaxGhenis left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings closed within scope (receipts in the fix comment); the exclusion-schema design stays with María per the TODO. Approving to dismiss my earlier request-changes.

@MaxGhenis
MaxGhenis merged commit e6be79a into main Aug 6, 2026
4 checks passed
juaristi22 added a commit that referenced this pull request Aug 6, 2026
…ning), #616

Brings Max's #617 adversarial-review fixes (preflight alias rejection,
fail-closed SDC floor, per-artifact audit isolation, strict JSON) and the
merged #610 base under this branch; the PR retargets main.

# Conflicts:
#	changelog.d/609-uk-weighted-integrity-gates.added.md
#	changelog.d/612-uk-frame-foundations.added.md
#	packages/populace-build/src/populace/build/uk_runtime/hmrc_source_contract.py
#	packages/populace-build/src/populace/build/uk_runtime/terminal_gates.py
#	packages/populace-build/src/populace/build/uk_runtime/weighted_integrity.py
#	packages/populace-build/tests/test_uk_efrs_weighted_totals.py
#	packages/populace-build/tests/test_uk_frame_linkage_preflight.py
#	packages/populace-build/tests/test_uk_rowwise_dataset.py
#	packages/populace-build/tests/test_uk_terminal_gates.py
#	packages/populace-build/tests/test_uk_weighted_integrity.py
#	packages/populace-data/src/populace/data/contract.py
#	tools/build_uk_national_dataset.py
#	tools/preflight_uk_frame_linkage.py
MaxGhenis pushed a commit that referenced this pull request Aug 7, 2026
…ulace Frames (stacked on #610 + #617) (#618)

* UK parity increment 4: weighted integrity gates in the terminal battery (#609)

Port the two incident-purchased US integrity gates to the UK per #609
(increment 4 of the #578 parity plan):

- Promote us_input_mass_totals to shared populace.build.input_mass (US
  import path re-exports; no fork).
- New uk_runtime.weighted_integrity: uk_dataset_input_mass_totals over the
  national tables, uk_input_mass_parity_gate (verbatim #278 semantics plus
  the universal stale-fails exclusion discipline the shared gate lacks,
  with the frozen reference identity recorded), and
  uk_qrf_tail_concentration_gate whose surface derives from the
  fit_weighted_qrf_stage* outputs declared in the HMRC source manifest —
  no sparsity filter, min_nonzero_records is the sole thinness guard.
- Both gates join uk_terminal_gate_report evidence-gated: unarmed paths
  omit them; an armed gate missing its reference or measured thresholds
  fails closed by name. Thresholds have no committed defaults (the #609
  measurement pass owes them) and are sealed into policy_sha256 with new
  input_mass_parity / qrf_tail_concentration evidence digests. Report
  schema 3, attestation 5; populace-data publication contract moved in
  lockstep, including detail-schema and stale-exclusion observables.
- Committed empty reviewed-exclusion registers, --input-mass-* /
  --qrf-tail-* staging launcher flags, a measurement tool
  (measure_uk_weighted_integrity_baselines.py), and an
  --emit-weighted-totals extraction for the pinned eFRS incumbent that
  refuses to write inside the repo pending the UKDS EUL confirmation.
- Mutation tests to the increment-1 standard: zeroed, 99.9%-loss, and
  concentrated columns each fail by name through the armed battery; a
  crash inside either evaluator appends a named failure without masking
  pending gates; dropping an evidenced gate fails membership validation.

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

* Disclosure-control the #609 measurement recorder; validate benunit nesting

The measurement recorder exists so its numbers can be posted on #578, which
makes its output a publication under UK Data Service End User Licence CD137
v16.00 clause 8: "To adhere to the statistical disclosure control standards
as detailed in the Research data handling and security guide for users in any
outputs I produce and publish." CD171-ResearchDataHandling §5.2.1 sets those
standards for Safeguarded data — never report cells based on one or two cases
(minimum threshold of 3, with 10 advised where several outputs share a
source), and "any output that refers to unit records, e.g. a maximum or
minimum value, must be avoided".

The recorder violated both in spirit: it emitted a per-column max_abs_value,
and it would have reported a top-k concentration share for a column with a
handful of carriers. Tail concentration is the (n, k) dominance statistic, so
at small k the share times the total approximates the mean of those few
records. Now: no maximum or minimum is emitted at all; a column with fewer
carriers than --sdc-minimum-count (default 10) reports no shares, no carrier
count, and no nonzero share; a --top-k narrower than that count is refused;
and the payload records the rules applied plus the clause 11-12 citation
obligations. Per-column weighted totals aggregate every carrier and remain
unconditional. A concentrated column above the threshold is still fully
visible, so the #462 signature the gate exists to catch is undimmed.

Separately, uk_dataset_input_mass_totals resolved a benunit's weight through
whichever member person came first, silently assuming benunits nest inside a
single household. The effective-mass coverage gate enforces that invariant,
but it is a different gate in a batched report, so this path now validates it
and fails closed by name instead of totalling a split benunit against one of
its households.

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

* Stop the totals run from republishing the coverage contract (#609)

--emit-weighted-totals ran after the reference writer, whose --output
defaults to the committed efrs_parity_reference.json. So measuring the
incumbent also regenerated the coverage contract against whatever
policyengine-uk happened to be installed — silently, as a side effect. With
policyengine-uk 2.89.4 locally against the committed 2.89.0 (871 engine
variables vs the recorded 866) that is a live diff, and the operator running
the #609 measurement pass has no reason to expect a contract rewrite.

--emit-weighted-totals is now its own mode: it writes the gate input outside
the repository and exits, leaving the committed reference untouched.
Regenerating the contract stays a separate, deliberate run.

With policyengine-uk installed the extraction is testable for the first time,
so it now has one: a synthetic three-table H5 with the incumbent's shape and
the sha pin stubbed proves the weight broadcast (distinct per-household
weights, so a broadcast error cannot pass), the exclusion of structural and
non-engine columns, the pinned identity block, that the emitted file loads
straight into uk_input_mass_parity_gate as a reference, and that a totals run
leaves the committed reference byte-identical. That path was previously
unexercised in the workspace and was about to be run against licensed data.

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

* Record what the incumbent measurement pass settled about thresholds (#609)

Ran the reference extraction against the pinned enhanced-FRS incumbent
(sha 584ae33d…, verified before reading) and measured its own concentration
baseline. The findings are written into the module docstring because they are
the reason the gates ship unarmed, and a later reader with the US constants
in hand would otherwise reintroduce them:

- The US 0.75 top-share threshold fails the incumbent on 16 of 28 checked
  columns at the US top_k/min_nonzero settings. UK reported-benefit columns
  sit on small, high-intensity subpopulations, so concentrated weighted mass
  is their normal state.
- No single global top-share threshold is both incumbent-compatible and
  incident-catching: the lowest no-headroom value on a broad surface is
  ~0.996, which would have missed the #462 incident itself (89% of mass in
  100 of 2,295 carriers).
- The incumbent cannot supply a QRF baseline at all. Eleven stage-1 outputs
  and five stage-2 disability columns are absent from it, and the seven
  stage-1 columns it carries hold survey-reported FRS values rather than
  donor draws, so that is a different distribution from the one the gate
  polices. The QRF threshold needs a staged candidate.
- A magnitude floor is not a coherent materiality filter on this surface,
  which mixes currency totals in the trillions with weighted person counts
  and flag counts. The US 1e9 floor would stop checking 50 of 131 columns,
  including gift_aid and charitable_investment_gifts — the two the release
  input coverage manifest requires distributional effective mass for. A 0.0
  floor still skips the three exact-zero reference columns, since the shared
  gate compares <=.

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

* Record the compact-vs-incumbent measurement result (#609)

The certified compact is not a valid candidate against the pinned incumbent,
which confirms the issue's reading of reference option 2. Measured: 8.1% less
household mass (28,840,551 against 31,389,678), 150 columns against the
reference's 131 with 22 candidate-only, and per-column drift with a 22.3%
median and a 578% maximum — a differently calibrated build rather than a
mass-preserving derivative. It also zeroes gift_aid and
charitable_investment_gifts and carries none of the eleven hmrc_spi_* columns,
because it is the input to the HMRC/SPI stage that creates them: those are
stage-1 QRF outputs. Comparing the two reproduces the #327 failure mode on the
restoration surface.

So both thresholds need a staged candidate, and the adjudicated 0.0 mass floor
stays uncommitted as a constant because a policy arms both together.

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

* Measure the production release pair: the release-arm baseline exists (#609)

The comparison production actually runs is shipped-release against next
shipping of the same kind — the _export_input_mass_gate pairing whose
reference #327 adjudicated to a certified release. The only consecutive
certified UK pair (72aeefc 2026-06-11 -> dd68c73 2026-06-19, both
sha-verified) measures it:

- The #278 incident class passed cleanly: zero columns dropped, zero zeroed,
  four added (reported, never failing). The gate's core protection needs no
  threshold judgment and would not have false-alarmed on a real shipping.
- A drift tolerance cannot be tight yet: 143 shared nonzero columns moved
  with median |drift| 10.52% and max +2,973.71% (person.adult_ema),
  dominated by intentional reported-benefit repopulation. Tolerances between
  5% and 50% would have failed 22-89 columns of a reviewed shipping. The
  no-headroom release-arm boundary is the pair's exact maximum, 29.737060,
  ratcheting down as consecutive same-code shippings stabilise — never up.

Recorded in the module docstring next to the other measurement findings so
the release-arm wiring (items 6/8) arms from evidence, not from the US 0.5.

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

* Pin UK gate behavior to the shared US implementations with differential tests

Review asked whether the UK port reinvented anything. The audit: gates.py and
the US release tool are byte-identical to main, both UK wrappers call the
shared gate functions, and us_input_mass_totals is the same function object
as the promoted helper. Three differential tests now lock that equivalence
against future drift:

- Equivalent records expressed as a populace Frame (the US path) and as UK
  national tables produce identical per-column weighted totals, pinned once
  to a hand computation so both paths anchor to the semantics, not merely to
  each other.
- Without exclusions, uk_input_mass_parity_gate reproduces the shared gate's
  verdict, failure lines, and details verbatim; the UK result may only add
  reference_identity and the stale/dormant register fields #609 requires.
- With exclusions, uk_qrf_tail_concentration_gate matches the shared tail
  gate's verdict, failures, and details exactly, differing only in the UK
  battery gate name.

The deliberate deltas stay what the issue mandated: stale-exclusion failures
on the input-mass side (the shared gate skips excluded columns) and no
sparsity filter on the QRF surface.

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

* Promote the engine-tables materializer into shared populace.frame.engine_tables

Both engine adapters carried a private copy of the same boundary rule:
typed weights are authoritative, and any existing {entity}_weight column
is overwritten in place, never trusted. The UK carrier swap (#612) needs
the same materializer without an adapter class, so the rule moves to one
shared function; the axiom adapter delegates unchanged, and the
PolicyEngine-US adapter pins weighted_entities=("household",) so its
export contract keeps materializing household weights only.

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

* Require an explicit weight kind on in-memory UK rowwise inputs

_dataset_tables defaulted a missing household_weight_kind to DESIGN, so
an IMPORTANCE- or CALIBRATED-weighted in-memory dataset handed to the
rowwise clone path was silently downgraded and written out with the
wrong populace_household_weight_kind attribute. In-memory datasets now
hard-fail on the absent attribute, mirroring the adjacent mass_log=None
rejection; H5 inputs keep their documented attribute-less design
default. Chartered under #612 to land before the carrier moves.

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

* Add the #612 UK Frame-linkage preflight: SDC-safe, read-only

Frame construction enforces invariants the UK path never checked (group
ids sorted ascending; membership equality in both directions, so a
person-less benunit fails, not just a dangling reference). This tool
classifies the real artifacts against every one of them before any
migration code runs, and its output is publishable under UKDS EUL
CD137 §8 / CD171 §5.2.1: threshold-guarded counts, booleans, and
aggregates only; Frame exception text (which embeds real ids) is never
echoed, and a planted-sentinel canary test holds that across stdout,
stderr, and the JSON file. Reads are mode="r" throughout so the
certified-candidate fingerprint survives.

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

* UK Frame carrier foundations: schema, provenance, loaders, one physical writer (#612)

national_frame.py is the UK build's Frame construction seam:
UK_NATIONAL_SCHEMA (the person table already uses Frame's exact
membership convention, so zero renames), uk_national_frame (typed
household weights built from the exported household_weight column,
which stays on the table to preserve column order), the metadata
accessors, validate_uk_national_frame (the UK residue: entity set, time
period, column-vs-typed-vector agreement, mass-log total agreement),
and UKStagingProvenance — provenance beside the frame, the
UKCertifiedCandidateIdentity idiom, because Frame metadata rejects
Path and Frame ops hard-construct Frame(...).

load_uk_national_frame / write_uk_national_frame land beside the shadow
carrier's loader and writer, sharing one extracted reader (suffix check,
symlink resolve, fingerprint race guard) and one extracted physical
writer (atomic temp+replace, tables + weight attrs landing together).
write_uk_rowwise_dataset routes through the same physical writer, so the
two artifacts can never drift in layout.

Acceptance, all in CI on synthetic tables: a payload-identity test
proving the Frame writer's artifact matches the shadow writer's (keys,
column order, dtypes, values, root attrs — deliberately not bytes; HDF5
stamps write times), a write/load round trip with provenance, and the
SPI-replacement frameability test — the mid-pipeline kill-shot showing
stage 2's cloned/rebuilt output satisfies Frame's sorted-group-id and
bidirectional-membership invariants.

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

* The UK rowwise clone path accepts a Frame input (#612)

_dataset_tables gains a Frame branch: tables through the shared
engine-tables materializer (typed weights authoritative), the weight
kind from the typed weights themselves, and the mass log from the
frame. Together with the earlier hard-fail on attribute-less in-memory
carriers, the silent DESIGN downgrade is now unreachable from either
input shape. UKRowwiseDatasetResult itself is unchanged — its own
carrier swap is out of #612's first increment.

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

* Guard the preflight's H5 fixtures for the pytables-free wheel gate

The wheel-packaging gate installs the built wheels into a clean venv
without pytables, where the four H5-writing preflight tests failed at
pd.HDFStore. They now importorskip tables and h5py, the repo convention
for H5-backed tests; the SDC masking test needs neither and still runs.

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

* The swap: UK national stages run Frame -> Frame (#612 commit 3)

UKNationalStage.transform is re-typed Callable[[Frame], Frame] and the
build loop loads (frame, provenance), validates the UK residue after
each stage, and feeds gate evidence as engine_tables(frame) — a plain
mapping all three gate families already accept, so terminal_gates.py
and weighted_integrity.py are untouched (#611 owns their typing).

The certified-candidate fence moves onto the provenance record: the
build binds it to any stage exposing bind_staging_provenance, and
_assert_provenance_matches_certified_candidate fails closed on an
unbound run — the same strength the smuggled _source_h5 fields gave,
without a carrier to smuggle them in.

Stage bodies follow the mass rule documented in national_frame.py:
the retained-leaves stage (person-only replacement) hard-constructs
with the input's kind and mass log carried through; the SPI stage
(reshapes tables AND advances DESIGN -> IMPORTANCE) hard-constructs
with the support seam's reviewed conservation record. The calibration
materializers take the national frame and read kind/period/tables
through the accessors; their inner constraint-frame constructions are
unchanged. uk_national_frame now also rejects a missing or blank time
period at construction, where the old carrier's validator caught it.

The shuffled FRS fixture keeps its teeth: group tables sort (Frame
raises on unsorted ids, never reorders) while the person table stays
shuffled, so lineage resolution must remain id-keyed — the 2024-25 FRS
bug class the fixture exists to catch.

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

* Rowwise consumers load Frames; carriage proofs use the rowwise reader (#612 commit 4)

tools/build_uk_rowwise_candidate.py loads the national input as a Frame
and feeds it straight into the ladder clone (commit 2's Frame branch);
the new construction and loading names are re-exported from the
uk_runtime package additively.

Converting the rowwise fixtures surfaced a boundary worth recording:
the rowwise clone artifact is NOT a national frame — clone_index lives
on every entity table, which Frame's global column-uniqueness rule
rejects. The attr-carriage proofs therefore go through the rowwise
seam's own reader (read_uk_single_year_weight_metadata), which is the
contract they were always pinning, and survives the shadow carrier's
retirement. UKRowwiseDatasetResult's own carrier design remains out of
scope, now with a concrete reason on record.

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

* Retire the UKNationalDataset shadow carrier (#612 commit 5)

The class, its loader/writer pair, and validate_uk_national_dataset are
deleted; the package re-exports swap to the Frame construction surface.
Hard removal, no deprecation shim — populace is 0.1.0 pre-release, and a
shim would keep the shadow type alive, which is what #612 exists to end.

The payload test's dual-writer arm retires with the shadow writer; it
now pins payload stability across write -> load -> write generations
(keys, column order, dtypes, values, root attrs), which is the surface
downstream readers actually depend on.

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

* Add the payload comparator for the old-vs-new staging acceptance (#612)

The instrument for the remaining pre-merge item: compares two UK
single-year H5 artifacts over the payload surface downstream readers
depend on — store keys in write order, per-table column order, dtypes,
index type and values, row order, values, and root attrs — because byte
identity was never attainable (HDF5 stamps write times). Thereafter it
is the standing acceptance instrument for any writer or carrier
refactor claiming the artifact did not change.

Fail-closed per the #617 review discipline: left/right and --json-out
alias rejection (resolved path + samefile, rechecked after the audit),
an un-lowerable SDC floor, differing rows reported as threshold-guarded
counts by column name (never values, canary-tested across stdout,
stderr, and the JSON file), and strict JSON. Exit 0 identical, 1
differing, 2 unsafe configuration.

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

* Gate evidence carries the frame's metadata surface (#612 acceptance finding)

The first credentialed acceptance build caught what CI's toy stages
could not: the coverage gate's hmrc_spi_income family getattr-reads
household_weight_kind, time_period, and mass_log off the evidence, and
the plain engine_tables mapping silently failed all three to ''/() —
kind '', period '', and a missing MassChangeRecord, three spurious gate
failures on top of the real findings. The build loop now materializes a
_UKGateEvidence adapter: the three engine tables plus the frame's
metadata, exactly the shadow carrier's read surface, in one place, so
the deliberately duck-typed gate modules (#611's charter) stay
untouched. The seam test's coverage fake now asserts the metadata attrs
so the gap cannot silently reopen.

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

* Harden the payload comparator against its adversarial review findings

Four verified false-negative holes and two fail-open surfaces, each now
closed with a regression test: int64 values above 2**53 compared equal
through a float64 cast (raw integer comparison now); a Series-vs-
DataFrame stored-kind difference was normalized away despite corrupting
what load_uk_national_frame parses (stored_kind is now payload); root
attrs compared as str() masked mid-array and typed differences (raw-
value equality, names-only reporting kept); index dtype and name were
unchecked. sdc_count and compare_tables now validate the CD171 floor
themselves, so a direct importer cannot disclose below-floor counts.
An unreadable artifact no longer exits 1 (the payloads-differ verdict)
with a raw traceback: the comparison is protected, exception text is
suppressed, and exit 2 means no-verdict. The disclosure canary now
also plants a sentinel inside the mass-log attr JSON.

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

* Restore the fence's descent guarantee and the review's remaining findings (#618 adversarial review)

The certified-candidate fence bound provenance to a mutable transform
attribute with no linkage to the frame being transformed: a stale
binding could fence a later run, and a pipeline whose first stage
consumed a substituted frame passed as long as a matching load had
happened once. bind_staging_provenance now binds the loaded frame too,
the retained-leaves transform records its input, the SPI fence requires
the pipeline to have started from the bound frame, and bindings are
single-use — strictly stronger than the retired loader-attached design,
which with_tables could carry past content replacement.

Also from the review: the driver's post-stage validate_uk_national_frame
seam gains its first direct test (a with_weights stage with a stale
exported column — constructs fine, only the driver seam rejects);
uk_national_frame stores the caller's exact time_period (validating on
a stripped copy) instead of rewriting padded payload; the rowwise clone
tests regain reader-side structural validation of the written bytes;
the changelog fragment now declares the float64 weight-column
canonicalization, the newly load-bearing Frame invariants, and the
result-surface renames; and the test module docstring that still
claimed a shadow-writer comparison arm points at the credentialed
acceptance evidence instead.

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

* Use ladder-appropriate reader-side checks in the ladder clone test

The review-response commit applied validate_uk_rowwise_dataset_tables to
both converted clone tests, but that validator demands the
rowwise-geography column family and the ladder artifact carries the
ladder's own columns — CI caught the mismatch. The ladder test now
asserts the structural facts that apply to its artifact from the
written bytes: entity row counts against the in-memory result, id
uniqueness, and person->household linkage. The rowwise-geography test
keeps the full rowwise validator, which is correct there.

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

* Evict Finder .DS_Store droppings and gitignore them

A directory-path git add in the review-response commit swept three
untracked .DS_Store files into the branch; they are Finder metadata,
not payload. Removed from tracking and ignored repo-wide so a future
add cannot repeat this.

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

* Address the #618 review blockers: seam revalidation, pinned export schema, digest-bound receipt

Blocker 1: Frame.table returns stored internals, so post-construction
mutation could pass validate_uk_national_frame and reach export. The
kernel gains Frame.revalidate() — every constructor invariant re-run
against current state — and the UK validator calls it first at every
seam, restoring the retired shadow validator's defensive recheck without
re-duplicating kernel logic in UK land. The reviewer's six injected
corruption classes are a parametrized regression test.

Blocker 2: the export schema is now enforced — validate rejects frames
carrying non-household typed weights (the loader refuses the reserved
columns they would materialize) and link-bearing schemas (the writer
would silently drop them); write_uk_national_frame additionally pins
engine_tables(weighted_entities=('household',)).

Blocker 3: the comparator compares dtype objects (a categorical ordered
flag now fails with an explicit note) and digest-binds both inputs by
sha256; the old-vs-new round-trip receipt is regenerated with the
hardened comparator and committed as
experiments/612-uk-carrier-payload-receipt.{json,md} — verdict, row
counts, digests, and provenance, verifiable offline. The two artifacts'
own digests differ while the payload is identical: the byte-vs-payload
doctrine in one receipt.

Plus ruff format on the flagged files.

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 10, 2026
The committed uk/degenerate_reviewed_exclusions.json register is the
degenerate-release-surface policy of record, carrying the #630
adjudication: the column is constant at the build vintage by
construction — lineage plumbing for the rowwise clone's
source_household_key that documents the vintage at the row level, not
signal. A None argument to uk_terminal_gate_report resolves to the
register (pass {} to run bare); stale entries still fail the gate, so
the register cannot rot. Interim schema-1, matching the two existing
registers; all three upgrade together when the #610 approval-identity/
receipt/expiry design lands.

The frozen policy digest moved with the register — the intended
tripwire — and the microcosm-data pin is now vintage-aware: new
releases must attest the #630 policy while the grandfathered June
release keeps attesting its own pre-#630 digest (defensive today, since
the terminal-report checker only runs for exact-k ids). The driver
gains --degenerate-exclusions as a review-time override, wired through
the distinct-path check.

Live receipt: the full-scale evidence run at seed 7 evaluated the
battery with the register active and degenerate_release_surface passed
with the exclusion recorded; only weight_ratio (1592.18 vs the 1151.25
maximum, its own #630 adjudication) still fails at full scale.

Refs #630, #627, #610.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
The committed uk/degenerate_reviewed_exclusions.json register is the
degenerate-release-surface policy of record, carrying the #630
adjudication: the column is constant at the build vintage by
construction — lineage plumbing for the rowwise clone's
source_household_key that documents the vintage at the row level, not
signal. A None argument to uk_terminal_gate_report resolves to the
register (pass {} to run bare); stale entries still fail the gate, so
the register cannot rot. Interim schema-1, matching the two existing
registers; all three upgrade together when the #610 approval-identity/
receipt/expiry design lands.

The frozen policy digest moved with the register — the intended
tripwire — and the microcosm-data pin is now vintage-aware: new
releases must attest the #630 policy while the grandfathered June
release keeps attesting its own pre-#630 digest (defensive today, since
the terminal-report checker only runs for exact-k ids). The driver
gains --degenerate-exclusions as a review-time override, wired through
the distinct-path check.

Live receipt: the full-scale evidence run at seed 7 evaluated the
battery with the register active and degenerate_release_surface passed
with the exclusion recorded; only weight_ratio (1592.18 vs the 1151.25
maximum, its own #630 adjudication) still fails at full scale.

Refs #630, #627, #610.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
Resolves the TODO(PR #610 review): every admitted exclusion now records
who approved it, the reasoning, the adjudication it descends from, when
it was approved, and when it expires. One frozen UKReviewedExclusion
record, one validator, one loader — shared by all three registers and
any future entry; nothing in the schema knows about source_year. The
whole record is sealed into the policy digest (74c9cd -> 2dbd78 ->
ae93bd10 across this PR's commits), so editing an approver or extending
an expiry moves the pinned literal.

Expiry is enforced at gate evaluation with an injected clock — never at
load, where the committed registers import at module load and a lapse
date would brick every build at once. An entry is honored through
expires_on; strictly after it, the exclusion stops suppressing and the
gate fails with one combined renew-or-remove message naming the
approver, the adjudication, and the lapse date. Details gain additive
expired_exclusions and exclusions_evaluated_on keys on all three gates,
and the contract requires expired_exclusions to be empty on published
reports (absent fields default to empty, so grandfathered reports stay
total).

The shared US-consumed gate layer and the contract's flat str->str pin
on QRF exclusion details are untouched: the UK wrappers project records
down to plain reasons for non-expired entries before delegating, and
withhold expired ones so the underlying failure fires beside the
expiry context. The degenerate gate consumes records directly and its
nested details entries carry the approver, adjudication, and expiry.

The source_year entry keeps Max's #630 adjudication with the reason
corrected to the verified mechanics: the column is derivable from the
artifact's time_period on a single-vintage build and is retained for
row-level vintage documentation and UK/US schema symmetry (the US pools
three ASEC vintages, where the same column is irreducible per-row
identity). approved_by juaristi22, expires 2027-02-10 — a plain
six-month review, since multi-vintage stacking would auto-retire the
entry through the stale discipline regardless.

Refs #610, #630, #609.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
The committed uk/degenerate_reviewed_exclusions.json register is the
degenerate-release-surface policy of record, carrying the #630
adjudication: the column is constant at the build vintage by
construction — lineage plumbing for the rowwise clone's
source_household_key that documents the vintage at the row level, not
signal. A None argument to uk_terminal_gate_report resolves to the
register (pass {} to run bare); stale entries still fail the gate, so
the register cannot rot. Interim schema-1, matching the two existing
registers; all three upgrade together when the #610 approval-identity/
receipt/expiry design lands.

The frozen policy digest moved with the register — the intended
tripwire — and the microcosm-data pin is now vintage-aware: new
releases must attest the #630 policy while the grandfathered June
release keeps attesting its own pre-#630 digest (defensive today, since
the terminal-report checker only runs for exact-k ids). The driver
gains --degenerate-exclusions as a review-time override, wired through
the distinct-path check.

Live receipt: the full-scale evidence run at seed 7 evaluated the
battery with the register active and degenerate_release_surface passed
with the exclusion recorded; only weight_ratio (1592.18 vs the 1151.25
maximum, its own #630 adjudication) still fails at full scale.

Refs #630, #627, #610.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
Resolves the TODO(PR #610 review): every admitted exclusion now records
who approved it, the reasoning, the adjudication it descends from, when
it was approved, and when it expires. One frozen UKReviewedExclusion
record, one validator, one loader — shared by all three registers and
any future entry; nothing in the schema knows about source_year. The
whole record is sealed into the policy digest (74c9cd -> 2dbd78 ->
ae93bd10 across this PR's commits), so editing an approver or extending
an expiry moves the pinned literal.

Expiry is enforced at gate evaluation with an injected clock — never at
load, where the committed registers import at module load and a lapse
date would brick every build at once. An entry is honored through
expires_on; strictly after it, the exclusion stops suppressing and the
gate fails with one combined renew-or-remove message naming the
approver, the adjudication, and the lapse date. Details gain additive
expired_exclusions and exclusions_evaluated_on keys on all three gates,
and the contract requires expired_exclusions to be empty on published
reports (absent fields default to empty, so grandfathered reports stay
total).

The shared US-consumed gate layer and the contract's flat str->str pin
on QRF exclusion details are untouched: the UK wrappers project records
down to plain reasons for non-expired entries before delegating, and
withhold expired ones so the underlying failure fires beside the
expiry context. The degenerate gate consumes records directly and its
nested details entries carry the approver, adjudication, and expiry.

The source_year entry keeps Max's #630 adjudication with the reason
corrected to the verified mechanics: the column is derivable from the
artifact's time_period on a single-vintage build and is retained for
row-level vintage documentation and UK/US schema symmetry (the US pools
three ASEC vintages, where the same column is irreducible per-row
identity). approved_by juaristi22, expires 2027-02-10 — a plain
six-month review, since multi-vintage stacking would auto-retire the
entry through the stale discipline regardless.

Refs #610, #630, #609.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
…610)

Addresses vahid-ahmadi's review of #658:

- UK_DEFAULT_DEGENERATE_REVIEWED_EXCLUSIONS and
  UK_TERMINAL_GATE_POLICY_SHA256 become cached accessors
  (uk_default_degenerate_reviewed_exclusions / uk_terminal_gate_policy_sha256)
  returning a MappingProxyType-wrapped register: the policy of record can
  no longer be mutated out from under the already-computed digest, and
  importing terminal_gates no longer reads the filesystem — a broken
  committed register surfaces as a clear ValueError at first use instead
  of an ImportError. The digest value is unchanged (ae93bd10…): laziness
  moves when the payload is read, not what it contains.
- The register-of-record test drops its prose pins ("schema symmetry",
  "derivable") — rewording the reason still moves the frozen digest, so
  the durable assertions are the structural receipt fields plus the
  microcosm#630 adjudication.
- New regression test: the register is loaded once, is immutable, and the
  digest is stable across calls.
- The unrelated failures.append reflow in contract.py is reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
…610)

Three independent adversarial lenses reviewed the PR; every cross-validated
finding lands here. The policy digest is unchanged (ae93bd10…) — these fix
behavior and validation, not the sealed payload.

- The degenerate gate now fails ANY out-of-force exclusion — dormant,
  signal-regained, or live — with receipt context, matching the input-mass
  and QRF wrappers (all three lenses found the dormant-expired silent
  pass: a green build would publish a report every consumer then rejects).
- Receipts gain an in-force window: an entry whose approved_on is still in
  the future is not an approval and never suppresses (previously fail-open
  for a typo'd future year). New premature_exclusions details key on all
  three gates, with contract expectations.
- Receipt dates must be canonical YYYY-MM-DD (fromisoformat also accepts
  compact and week-date forms; the raw string is sealed, so two spellings
  of one date minted two digests) and text fields must be trimmed.
- uk_terminal_gate_report coerces and freezes the register once at entry:
  the gate and the attested digest can no longer observe different
  contents when caller-controlled evaluators mutate the mapping mid-report.
- The driver loads the degenerate register (override or committed
  preflight) before the destructive sidecar unlinks — a typo'd
  --degenerate-exclusions path no longer destroys the previous build's
  evidence first — and the build record notes committed-vs-override
  register provenance (content-addressed digests can't show it).
- Contract: the three new detail fields join the required schema (a
  key-signed report can no longer omit them to dodge the empty-list
  expectations, and the absent-field defaults that had silently weakened
  four pre-existing checks revert to strict); exclusion-consuming gates
  must share one exclusions_evaluated_on; the legacy policy pin gains a
  lockstep test (it was asserted nowhere).
- exclusion_evaluation_date() refuses datetimes (a date subclass that
  compared timestamps against dates or leaked a timestamp into details).
- Stale Mapping[str, str] annotation on build_uk_national_dataset fixed.

Refuted during review (no change needed): duplicate JSON keys are rejected
at every nesting level by the loader's object_pairs_hook; the attested
policy_sha256 is recomputed from the actual register in force, so an
override cannot ride the certified pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
The committed uk/degenerate_reviewed_exclusions.json register is the
degenerate-release-surface policy of record, carrying the #630
adjudication: the column is constant at the build vintage by
construction — lineage plumbing for the rowwise clone's
source_household_key that documents the vintage at the row level, not
signal. A None argument to uk_terminal_gate_report resolves to the
register (pass {} to run bare); stale entries still fail the gate, so
the register cannot rot. Interim schema-1, matching the two existing
registers; all three upgrade together when the #610 approval-identity/
receipt/expiry design lands.

The frozen policy digest moved with the register — the intended
tripwire — and the microcosm-data pin is now vintage-aware: new
releases must attest the #630 policy while the grandfathered June
release keeps attesting its own pre-#630 digest (defensive today, since
the terminal-report checker only runs for exact-k ids). The driver
gains --degenerate-exclusions as a review-time override, wired through
the distinct-path check.

Live receipt: the full-scale evidence run at seed 7 evaluated the
battery with the register active and degenerate_release_surface passed
with the exclusion recorded; only weight_ratio (1592.18 vs the 1151.25
maximum, its own #630 adjudication) still fails at full scale.

Refs #630, #627, #610.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
Resolves the TODO(PR #610 review): every admitted exclusion now records
who approved it, the reasoning, the adjudication it descends from, when
it was approved, and when it expires. One frozen UKReviewedExclusion
record, one validator, one loader — shared by all three registers and
any future entry; nothing in the schema knows about source_year. The
whole record is sealed into the policy digest (74c9cd -> 2dbd78 ->
ae93bd10 across this PR's commits), so editing an approver or extending
an expiry moves the pinned literal.

Expiry is enforced at gate evaluation with an injected clock — never at
load, where the committed registers import at module load and a lapse
date would brick every build at once. An entry is honored through
expires_on; strictly after it, the exclusion stops suppressing and the
gate fails with one combined renew-or-remove message naming the
approver, the adjudication, and the lapse date. Details gain additive
expired_exclusions and exclusions_evaluated_on keys on all three gates,
and the contract requires expired_exclusions to be empty on published
reports (absent fields default to empty, so grandfathered reports stay
total).

The shared US-consumed gate layer and the contract's flat str->str pin
on QRF exclusion details are untouched: the UK wrappers project records
down to plain reasons for non-expired entries before delegating, and
withhold expired ones so the underlying failure fires beside the
expiry context. The degenerate gate consumes records directly and its
nested details entries carry the approver, adjudication, and expiry.

The source_year entry keeps Max's #630 adjudication with the reason
corrected to the verified mechanics: the column is derivable from the
artifact's time_period on a single-vintage build and is retained for
row-level vintage documentation and UK/US schema symmetry (the US pools
three ASEC vintages, where the same column is irreducible per-row
identity). approved_by juaristi22, expires 2027-02-10 — a plain
six-month review, since multi-vintage stacking would auto-retire the
entry through the stale discipline regardless.

Refs #610, #630, #609.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
…610)

Addresses vahid-ahmadi's review of #658:

- UK_DEFAULT_DEGENERATE_REVIEWED_EXCLUSIONS and
  UK_TERMINAL_GATE_POLICY_SHA256 become cached accessors
  (uk_default_degenerate_reviewed_exclusions / uk_terminal_gate_policy_sha256)
  returning a MappingProxyType-wrapped register: the policy of record can
  no longer be mutated out from under the already-computed digest, and
  importing terminal_gates no longer reads the filesystem — a broken
  committed register surfaces as a clear ValueError at first use instead
  of an ImportError. The digest value is unchanged (ae93bd10…): laziness
  moves when the payload is read, not what it contains.
- The register-of-record test drops its prose pins ("schema symmetry",
  "derivable") — rewording the reason still moves the frozen digest, so
  the durable assertions are the structural receipt fields plus the
  microcosm#630 adjudication.
- New regression test: the register is loaded once, is immutable, and the
  digest is stable across calls.
- The unrelated failures.append reflow in contract.py is reverted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
…610)

Three independent adversarial lenses reviewed the PR; every cross-validated
finding lands here. The policy digest is unchanged (ae93bd10…) — these fix
behavior and validation, not the sealed payload.

- The degenerate gate now fails ANY out-of-force exclusion — dormant,
  signal-regained, or live — with receipt context, matching the input-mass
  and QRF wrappers (all three lenses found the dormant-expired silent
  pass: a green build would publish a report every consumer then rejects).
- Receipts gain an in-force window: an entry whose approved_on is still in
  the future is not an approval and never suppresses (previously fail-open
  for a typo'd future year). New premature_exclusions details key on all
  three gates, with contract expectations.
- Receipt dates must be canonical YYYY-MM-DD (fromisoformat also accepts
  compact and week-date forms; the raw string is sealed, so two spellings
  of one date minted two digests) and text fields must be trimmed.
- uk_terminal_gate_report coerces and freezes the register once at entry:
  the gate and the attested digest can no longer observe different
  contents when caller-controlled evaluators mutate the mapping mid-report.
- The driver loads the degenerate register (override or committed
  preflight) before the destructive sidecar unlinks — a typo'd
  --degenerate-exclusions path no longer destroys the previous build's
  evidence first — and the build record notes committed-vs-override
  register provenance (content-addressed digests can't show it).
- Contract: the three new detail fields join the required schema (a
  key-signed report can no longer omit them to dodge the empty-list
  expectations, and the absent-field defaults that had silently weakened
  four pre-existing checks revert to strict); exclusion-consuming gates
  must share one exclusions_evaluated_on; the legacy policy pin gains a
  lockstep test (it was asserted nowhere).
- exclusion_evaluation_date() refuses datetimes (a date subclass that
  compared timestamps against dates or leaked a timestamp into details).
- Stale Mapping[str, str] annotation on build_uk_national_dataset fixed.

Refuted during review (no change needed): duplicate JSON keys are rejected
at every nesting level by the loader's object_pairs_hook; the attested
policy_sha256 is recomputed from the actual register in force, so an
override cannot ride the certified pin.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
juaristi22 added a commit that referenced this pull request Aug 11, 2026
source_year reviewed exclusion + schema-2 approval receipts with expiry (#630, #610)
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 parity increment 4: port the weighted integrity gates (input-mass parity + QRF tail concentration) into the UK terminal battery (#578)

2 participants