Skip to content

Make the UK fiscal-event replay select a certified bundle - #159

Draft
MaxGhenis wants to merge 11 commits into
mainfrom
replay/track1-rerun
Draft

MaxGhenis wants to merge 11 commits into
mainfrom
replay/track1-rerun

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Refs #156 (track 1 rerun). This PR is infrastructure only. It lets the UK fiscal-event replay select a certified bundle, so the five track-1 events can be rerun on the next certified pair. That pair is uk-data 1.58.0 with policyengine-uk 2.125.1 and core 3.33.0, certified by PolicyEngine/policyengine.py#555 (merged) and published as policyengine 6.2.6. The PR registers no new bundle and computes no new results, and every existing output byte is unchanged.

What changes

  • Bundle registry.

    • data/uk/certified_bundles/index.json maps a key to a pin file. Its only and default entry is populace-uk-2023__pe-uk-2.89.2 → the unchanged data/uk/certified_bundle.json (The pin follows the certified world — one rule, five registries #126).
    • pipeline/register_uk_certified_bundle.py writes a new pin from an installed policyengine.py release's packaged manifest and the cached HF artifact. It checks digest, size and model/core pins, and refuses any mismatch.
    • pipeline/export_uk_bundle_requirements.py exports the exact uk-extra freeze from a policyengine.py tag's uv.lock.
  • --bundle KEY on every replay stage:

    • the stages: build, compute (including --dry-run), stage, compare (including --summary), adopt, Modal, model diagnostics and the construction audit;
    • non-default paths: registries go to data/uk/events/bundles/<key>/ and results to results/uk/events/bundles/<key>/;
    • isolation: every receipt binds the key, pin digest, dataset, model, core and loader versions, and year window, and a stage refuses another bundle's inputs.
  • policyengine 6.x managed-loader adapter, alongside the 5.0.2 path:

    • resolves the HF model repo type;
    • checks the exact digest of the materialised runtime file before and after each simulation;
    • opens HDF read-only (no writable mirror);
    • falls back to the packaged certification when the network is blocked;
    • projects from the stored data year, which avoids the policyengine.py#557 trap.

    Each fact is checked against installed source in a source-hash-bound offline audit.

  • Year windows from the pin. The window runs from the H5's own time_period to the engine's extension end. Earlier OBR cells stay in the accounting once as outside_bundle_window.

  • Modal adapter takes its pins, HF cache path, freeze, audit, allowlist, outputs and window from the bundle. It keeps the resource caps, blocked runtime network, no forwarded credentials, and the pre-import and pre/post-simulation hash gates.

  • pipeline/compare_uk_engines.py writes results/uk/events/ENGINE_COMPARISON.{md,json,csv}, plus full input-hash provenance.

    • Each row is one event × measure × head × source FY: OBR, PE(base), PE(new), the change, both bins, status, and the new/base ratio of the head's certified aggregate next to the measure's own ratio.
    • Attribution comes only from a hand-authored file with a closed driver vocabulary. Unsized drivers never enter an explained share.
  • Paired synthetic diagnostics at both pins reproduce policyengine-uk #2237 / #2238 / #2239, the HICBC opt-out and the NICs threshold-freeze end date.

  • Construction adjustment (non-default bundles only). When the engine's CGT rate parameters carry fiscal_year_blend metadata, processed CY2024 already blends in the 30 October 2024 increase. The AB2024 CGT reversal then restores the old rates from 1 January 2024, which turns the 2.89.2 timing gap into a computed part-year row. 2025 onward is unchanged. This is recorded as construction_adjustments in the registry.

Invariants (tested; pytest + Hypothesis)

  • Default byte identity. The default bundle reproduces every existing registry, numerical receipt, staging, comparison and summary byte. The PR's validation reran all five registry --check runs, all five dry-runs, staging and comparison, --summary and the AB2025 checks.
  • Bundle isolation. No stage accepts another bundle's registry, artifacts, manifests or receipts.
  • Accounting across bundles. Per event, the source-row multiset and the exact Decimal net and absolute OBR £ are identical; only classifications and constructions may differ, and each difference is listed.
  • Window. There is no computed row outside the pin's window, and each pre-window cell is staged exactly once.
  • Engine comparison:
    • swapping base and new negates every change;
    • head changes sum to the measure change;
    • bins are invariant to a common positive scale;
    • no row is dropped or duplicated;
    • output is deterministic;
    • attribution is rejected when it is missing, outside the vocabulary, or sized without a hash-bound computed artifact.
  • Existing replay invariants still hold: sign orientation, reversal involution, head sums, inert-construction blocking and determinism.

Validation

  • Replay suite: 315 passed, 1 skipped (test_uk_event_*, test_modal_uk_event, test_uk_bundle_*, test_uk_engine_comparison, test_uk_replay_legacy, test_uk_ab2025_pipeline).
  • Byte identity: re-ran all five default registry --check runs and default stage/compare/--summary; git status shows no tracked change under data/ or results/.
  • Adapter checks on an already-certified bundle, published policyengine 6.2.5 (uk-data 1.56.16, policyengine-uk 2.102.3, core 3.32.10). These are adapter observations, not results.
    • Local: one baseline and one dated reform, run sequentially. The data sha256 was e433e532… before and after, the runtime source was the exact hashed file, and the network was blocked. Timings were 129 s and 95 s.
    • Remote: one Modal --preflight-only run with no simulation, 5.8 s.
  • Nothing was computed on any uncertified pairing.

Not in this PR

Registering the 6.2.6 bundle (and the follow-up bundle that will carry policyengine-uk #2240–#2242), the five event reruns, the authored attribution and ENGINE_COMPARISON.md all come in the next PR. This one changes no app or DB build, so merging publishes nothing.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 10, 2026 07:41
Every replay stage (build, compute, stage, compare, adopt, Modal, model
diagnostics, construction audit) takes --bundle KEY. The default key is the
unchanged 2.89.2 pin, so every existing output byte is reproduced.
Non-default bundles get their own registry and results namespace. Each
receipt binds the key, pin digest, dataset, model/core/loader versions and
year window, and a stage refuses another bundle's inputs.

Adds a policyengine 6.x managed-loader path alongside 5.0.2, pin-derived
year windows (earlier OBR cells stay as outside_bundle_window), registration
from a release's packaged manifest, and an exact uk-extra freeze exporter.
Adoption's engine-free guard now checks only for engines imported during
validation, so it holds in an engine-equipped full test run.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compare_uk_engines.py writes ENGINE_COMPARISON.{md,json,csv} with full
input-hash provenance. Each event x measure x head x source-FY row carries
OBR, PE(base), PE(new), the change, both PE/OBR bins, and computed / window /
construction status. It also shows the head's certified-aggregate ratio
beside the measure's own ratio. Attribution comes only from an authored file
with a closed driver vocabulary, and unsized drivers never enter an
explained share.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RECIPE.md gains the ordered rerun procedure: register, freeze, registry
rebuild and construction re-audit, Modal preflight, determinism repeat, full
runs, adoption, staging, the engine comparison and the attribution file.
The 6.x loader audit and year evidence come from checks on the published,
already-certified policyengine 6.2.5 / uk-data 1.56.16 bundle. Those are
adapter observations, not results.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
policyengine-scorecard Ready Ready Preview Oct 10, 2026 1:50pm UTC

Request Review

stage_uk_event.main validated the registry against the default bundle, so
staging any non-default bundle failed with "bundle identity mismatch:
bundle_key". The library-level tests call stage_event directly and missed
it. This adds a CLI-level regression test that fails without the fix. Found
by staging the first real policyengine 6.2.6 event run.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compare_uk_engines checked the bundle identity at the top level of
MODAL_RECEIPT.json, but the Modal wrapper records it under "request". Every
non-default bundle with a real receipt was refused ("missing bundle identity
field"). The default bundle tolerates missing fields, and no fixture wrote a
receipt, so tests never reached this check. The two-bundle setup now lives in
a shared helper, and a new test covers receipts in the real shape plus a
foreign bundle key. Found by running the first real 6.2.6 comparison.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 5 commits October 10, 2026 08:49
validate_bundle_path only checked a path against its own kind's root, so a
results output could land in the registry tree. The review's example was the
default staging CLI with --output set to another bundle's registry. It now
checks data/uk/events and results/uk/events for every kind: a path in either
tree must sit in the selected bundle's namespace, and of the matching kind.
The diagnostics CLI also checks --output and --report before any engine
work, and a non-default bundle can't write the default bundle's
MODEL_INVESTIGATIONS.md. (Review findings 2 and 3 on #159.)

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Modal input manifest hashed the whole shared bundle index, so
registering any later bundle changed every earlier run's input commitment.
That made adoption of a completed run fail although none of its inputs had
changed. The wrapper now hashes and mounts a deterministic index holding only
the default mapping and the selected bundle's mapping. It is serialized
exactly as registration writes the index, so a run made while no other
bundle was registered keeps its commitment. (Review finding 6 on #159.)

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audit hook denied only socket.connect, so name resolution
(getaddrinfo and friends) and UDP sendto/sendmsg still reached the network.
The hook now denies those events too. A child-process test checks that DNS,
UDP and TCP all fail with the block message while a local socketpair (used
by multiprocessing) still works. (Review finding 8 on #159.)

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sized driver previously needed only a hash-correct artifact with the right
arithmetic. A dry-run artifact from another engine and dataset could
therefore produce an explained share. Sized evidence must now be an artifact
listed in a verified run manifest of a compared bundle and must pass that
bundle's runtime-identity check; this covers a computed artifact and both
endpoints of a paired run. The writer verifies every bundle's receipt chain
before it builds any row.

Sized drivers within one entry also can't cover the same heads of the same
measured endpoints. A two-head paired size beside its one-head subset, or a
measure total beside one of its heads, is rejected instead of being added
up. (Review findings 4 and 5 on #159.)

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The audit's CGT and SDLT findings were fixed prose written for 2.89.2. On
policyengine-uk 2.125.1 the "no asset-type, BADR or carried-interest branch"
statement is false: the formula has those branches, and it is the dataset
that lacks their inputs. Non-default bundles now get each statement from the
installed engine's source and the H5's columns, with formula capability and
population inputs reported separately. The historical bundle keeps its
committed findings verbatim. (Review finding 7 on #159.)

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Contributor Author

Independent review (GPT-6.1 Sol, read-only) of 61c13e3..93f2200: NEEDS-FIXES, eight findings. All eight are addressed in a858a69..a6d1bb5, plus one bug the review didn't cover. A delta review of the fix commits is running.

# Finding Fix
1 (blocking) Staging CLI validated every registry against the default bundle a858a69: pass --bundle; CLI-level test
2 (blocking) An output path outside its own kind's root could overwrite another bundle's registry f5cd310: every path is checked against both protected trees
3 (blocking) Diagnostics --report bypassed the namespace guard f5cd310: --output and --report validated before any work
4 (major) A hash-correct dry-run artifact from another engine could size attribution 7284ef7: sized evidence must be in a verified run manifest and match that bundle's runtime identity
5 (major) Overlapping sized drivers were added up 7284ef7: drivers can't cover the same heads of the same measured endpoints
6 (major) Registering a new bundle changed earlier runs' Modal input commitment bcd568c: only the selected bundle's index entries are mounted and hashed
7 (minor) Construction audit asserted 2.89.2 CGT mechanics for 2.125.1 a6d1bb5: derived from engine source and dataset columns for non-default bundles
8 (minor) Network block covered only socket.connect 3b7e1d3: DNS lookups and datagram sends are blocked too

Also 8c8ca52: the engine comparison read the Modal receipt's bundle identity at the top level, but the wrapper records it under request. Running the first real policyengine 6.2.6 comparison exposed this; the same runs exposed finding 1 independently.

Each fix has a test that fails without it. Full suite at a6d1bb5: 1568 passed, 1 skipped. Regenerating the default bundle's staging, comparisons and summary changes no tracked byte.

One head, one sized driver. A computed artifact and a paired run that ends
at it could still size the same head, because the two proof kinds were
tracked under different identities. Within an entry, each head can now be
sized by at most one driver, whatever proof it cites.

The engine comparison's output directory is checked before any work. It may
be the shared results/uk/events directory or a path outside both protected
trees, never a registry, an event's results or a bundle namespace.

The diagnostics CLI refuses the default bundle's investigation report for
--output as well as --report. Its test now runs against a temporary root:
with the guard removed, the earlier version really did overwrite the tracked
report.

Refs #156.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 7d172b99 Deployed Oct 10, 2026 by vercel[bot]
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.

1 participant