Skip to content

Export certified UK event replays for the Autumn Budget dashboard - #227

Merged
MaxGhenis merged 9 commits into
mainfrom
uk-events-dashboard-export
Oct 25, 2026
Merged

MaxGhenis merged 9 commits into
mainfrom
uk-events-dashboard-export

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

The scorecard side of showing past UK fiscal events in the Autumn Budget dashboard (PolicyEngine/uk-autumn-budget-dashboard-2026). The dashboard costs every figure through its own publication pipeline, on its own certified model and data pair. From a replay it takes how each measure was built, plus the replay's numbers for a differential check. Nothing here changes a replay, a registry or a seal.

What it adds

  • pipeline/export_uk_events_dashboard.py. From a certified replay namespace, it writes one file per event (results/uk/dashboard_export/<variant>/<event>.json) and an EXPORT_MANIFEST.json of digests. Each file holds:
    • every registry measure, with its class, gap kind and reason;
    • each computed measure's construction: the dictionaries the replay submitted to each world, entry points, construction revision, sentinels, fiscal heads, and executor_route. The route is uc_health_element_refresh when compute_uk_event.uc_health_scenario refreshes the UC cohort.
    • its per-year totals and processed parameter values;
    • the OBR side of every source row, with the replay's own counterpart from COMPARISON.csv named replay_* (including replay_ratio_bin);
    • the registry's source-inventory accounting, recomputed with arithmetic that refuses to round and required to equal the registry's.
  • The committed export for pe6.9.0_uk2.130.1_policyengine-uk-data-1.58.1: six events, 426 measures, 25 of them computed.
  • docs/uk_replay/DASHBOARD_EXPORT.md: the contract, what the numbers mean (each measure at the margin of the replay engine's current law, not the estimates made at the time) and how the dashboard uses the file.

Before anything is written:

  • The variant record must validate, and its recomputed identity digest must match the receipt. The receipt and its integrity status must both be complete.
  • Every input must be under the seal: receipt → namespace manifest → file digest, and run manifest → registry digest. A sealed file that is missing is refused.
  • The corpus and the namespace manifest must name the same events, so a missing event fails the export rather than dropping out of it.
  • Each run's identity, data years and supported years must match the variant and registry. Artifact measure/years must be unique and equal the supported-year grid of the computed measures. A measure that is not computed cannot own constructed OBR rows.
  • The submitted worlds must equal the registry's construction under annual_reform. A measure with a construction revision gets no exemption: the recorded revision is applied, its evidence must match the revision registry, and the submission may differ from the registry's only by that revision's fiscal-window change (as compute_uk_event.revision_reform_route makes it). Every artifact must agree, not just the union.
  • Legacy and uncertified variants are refused.

The exported dictionaries are the replay's literal submissions, so their timing depends on the engine and entry point. The doc tells an importer not to pass them through annual_reform again (that would move CGT's first year from 2024 to 2025), and to compare a per-year parameter readback with each world's processed_parameter_values instead.

No household-level value is read or written.

Invariants (tests/test_export_uk_events_dashboard.py)

  • Regeneration. The committed export regenerates byte for byte, and the manifest names each file by its digest.
  • Coverage.
    • Every registry measure appears once.
    • A measure is computed exactly when the registry computes it and the seal holds its artifacts, with one year per supported year.
    • A measure that is not computed has a reason.
  • Construction direction. A reversal submits a baseline-world dictionary and runs current law as its reform world; a forward delta does the opposite.
  • Totals. Each year's total is the sum of its head effects.
  • Accounting.
    • Classes partition the source rows, so absolute and measure shares each sum to exactly 1 (shares are exact fractions).
    • The recomputed totals equal the registry's recorded input and classified rows, net and absolute GBP, overall and for each fiscal year, with the same fiscal years.
    • Statuses also partition the rows.
    • The constructed rows' absolute GBP is no more than the computable classes'.
  • Closed shape. Every nested object is checked recursively against a closed schema; an unknown field anywhere is refused.
  • Execution route. The receipt-bound engine version agrees with the sealed construction premises, each run manifest and every artifact. A revised measure's submission is the one its engine's route makes: fiscal windows where Scenario.from_reform sets dated_reforms, annual windows otherwise, from SCENARIO_DATED_REFORMS, a table of verified engine versions (a version it does not list is refused). Each replay-engine leg of CI checks the table's entry against its own engine's Scenario.from_reform (--check-scenario-capability), and refuses an unlisted engine for a variant with a committed export. Every artifact's entry point matches its submitted world, as compute_uk_event.run_sim sets it.

Hypothesis properties cover:

  • exact decimal text;
  • shares of any partition sum to 1;
  • sums never round;
  • merging per-year dictionaries is order-free, and refuses disagreement.

The whole accounting output, gap-kind accounts included, is checked against an independent Fraction implementation over generated registries whose row classes and gap kinds vary independently of their measure's, which is a differential check. Tampered copies are refused: an artifact, COMPARISON.csv, the namespace manifest, the registry, an identity path changed without its digest, unsealed data years, an incomplete integrity receipt, a deleted event, a missing or duplicate artifact year, and a revised measure whose CGT rates were all changed from 0.1 to 0.99.

Local run at 9c9859a: the export tests give 85 passed and 1 skipped. The skip is the engine-capability test, which needs policyengine-uk; run with policyengine-uk 2.130.1 installed, it passes. The 18 new cases fail against 4e11f68's exporter. Ruff format and check: clean. Regenerating with the CLI reproduces the committed export byte for byte, so the dashboard's imported registries need no change. The full suite was last run locally at 4e11f68 (2542 passed, 17 skipped, and 1 environmental failure, test_modal_factory_has_one_blocked_network_worker_and_explicit_uv_venv, which needs a Python 3.12 control interpreter); CI is the reference.

bd87483, 071e75a and 9a4d2ca move that check into the exporter (--check-scenario-capability), which asks the replay's own revision_reform_route on the installed engine, and run it on every replay-engine leg; a test pins the step's whole shape, and engine-free tests cover each outcome. They were not run locally (the host is under a disk hold this weekend), so CI is their check. At 071e75a: 2568 passed and 18 skipped; the three uk2.130.1 legs report dated_reforms=True, as recorded, and the uk2.89.2 and uk2.125.1 legs, which have no export, report that the export refuses them.

Review

Round 1 (GPT-6.1 Sol, at e911671) requested changes, with nine findings. d665d08, 2a47913, 490bcef and 4e11f68 fix them. The committed export keeps every value: the event files differ from e911671 only by the ratio_bin → replay_ratio_bin rename (6,096 keys, checked by a structural diff), and the manifest only by the six digests that follow. Round 2 (GPT-6.1 Sol, at 4e11f68) found eight of the nine fixed and one partly fixed: a revision's submission was not bound to the engine's execution route, so annual windows recorded under UK 2.130.1, or a nonempty reform marked certified_current_law, were accepted. 9c9859a fixes it, with the reviewer's two accepted fixtures, fully resealed, as regression tests. Round 3 (GPT-6.1 Sol, at 9c9859a) approved: both resealed route attacks are refused, and all nine round-1 findings are fixed. Its one risk was that the engine-capability test skipped in every CI job; bd87483 fixes that. An independent Opus 5.5 review of bd87483 approved it with eight nits, all fixed in 071e75a (without editing compute_uk_event.py, whose digest the determinism records hold); its review of 071e75a approved it with two test nits, fixed in 9a4d2ca, and it approved 9a4d2ca. CI at 9a4d2ca passes every check.

Not in this PR

  • A replay on the dashboard's pair. The dashboard runs policyengine 6.10.2, policyengine-uk 2.132.6 and Microcosm UK 2024-25, and that replay waits on Run the UK replay's year workers that fit the runner's memory, and log memory around each event #218. Until it exists, the dashboard's differential check is parameter-level. Once it is certified, --variant exports it.
  • Exporting automatically for each newly certified variant. The results-PR builder's scope check refuses edits outside its list on purpose, so the export stays a deliberate, committed step per variant.

axiom: n/a: replay export tooling, no policy encoded

🤖 Generated with Claude Code

pipeline/export_uk_events_dashboard.py writes, from a certified replay
namespace, one file per event: every registry measure with its class and
reason, each computed measure's construction (the parameter dictionaries
the replay submitted to each world, entry points, sentinels, construction
revision, executor route and fiscal heads), its per-year totals and
processed parameter values, the OBR side of every source row with the
replay's own counterpart from COMPARISON.csv, and the registry's
source-inventory accounting recomputed exactly. Everything read is bound
to the seal (receipt, namespace manifest, file digests) and to the
registry digest each run recorded; a mismatch is refused.

The committed export is for pe6.9.0_uk2.130.1_policyengine-uk-data-1.58.1:
six events, 426 measures, 25 computed. Tests check it regenerates byte for
byte, plus coverage, construction direction, head totals, the accounting
partition and a closed document shape, with Hypothesis properties and a
Fraction reference for the accounting, and refusals on tampered copies.

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

vercel Bot commented Oct 24, 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 25, 2026 1:11am UTC

Request Review

MaxGhenis and others added 4 commits October 24, 2026 12:10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The export picks a revision submission from SCENARIO_DATED_REFORMS, and the
only test that compared that table with a real engine skipped in CI: the
pytest job installs no engine, and the engine legs never ran it. The
exporter now has --check-scenario-capability, which compares the table's
entry with the installed engine's Scenario.from_reform, and each
replay-engine leg runs it on its variant's own environment. An unrecorded
engine passes there unless the variant has a committed export, which the
export would refuse. Engine-free tests cover the check's three outcomes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of bd87483 (approved, with nits):
- The check re-typed the route's Scenario predicate. It now calls
  compute_uk_event.revision_reform_route with a revision of the probe's
  path, so it tests what the replay does, without touching the sealed
  replay code.
- A test pins the step to every replay-engine leg, unconditionally,
  after the environment is verified.
- --check-scenario-capability takes the newest certified variant when no
  --variant is given, as the help says, refuses an engine that is not the
  variant's, and reports a missing engine instead of a traceback.
- The success-path tests refuse any export.
- Comments, the doc paragraph and the step name now say what the check
  does.

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

Review of 071e75a (approved, with nits): the pin test now refuses any
key beyond name and run, so continue-on-error, env or shell cannot weaken
the step; the default-variant test asserts main's exit; a test reaches
main's message for an unrecorded engine; the docstring names the missing
engine case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 25, 2026 01:20
@MaxGhenis
MaxGhenis merged commit 74fcc68 into main Oct 25, 2026
13 checks passed
@MaxGhenis
MaxGhenis deleted the uk-events-dashboard-export branch October 25, 2026 01:22
MaxGhenis added a commit that referenced this pull request Oct 25, 2026
The independent review of 661c14d asked for eight changes. This commit
makes the code and text changes; the artifact, database and feeds are
regenerated by a rerun after the fleet's disk hold lifts, so the
artifact-reading tests fail until then.

- Runtime dataset: each world hashes the file the managed simulation
  reports as runtime_dataset_source (policyengine 5.0.2 sets it in
  managed_microsimulation), before construction and after its reads, and
  requires the runtime release bundle and dataset years to be the
  variant's. The run goes through compute_uk_event's preflight for the
  variant (legacy or materialised, new --variant flag).
- Variant identity: verify_installed and verify_import_origins before and
  after the run; the artifact records package versions and RECORD hashes,
  Python, the packaged manifest and the release bundle, and ingest
  refuses an artifact whose pairing differs from its variant record
  (schema_version 2).
- England: both offers are England's, and policyengine-uk 2.89.2 applies
  Tax-Free Childcare UK-wide, so every head is also summed for people in
  English households and the counterparts use those sums. Ingest refuses
  a free-hours change outside England and states the Tax-Free Childcare
  change outside England on each result.
- Claim ids: the printed value moves from conditions to publication, so a
  corrected extraction keeps every id (property test).
- Annotation bases: the static basis now cites the mechanism the engine
  uses (employment_income adds employment_income_behavioral_response,
  which is zero because both elasticities are zero); CBP's friction rule
  cites pages 7 and 9, now in the extract; the full-utilisation note
  rests on the take-up and usage inputs the run records.
- Metadata: is_child, is_child_receiving_extended_childcare, country,
  the take-up inputs and childcare_expenses are recorded; a test reads
  every calculate() name off the source.
- History: ingest keeps every certified artifact's run, newest latest,
  so the README's promise holds when a later variant arrives.
- Adapters emit the tidy-row core (program, metric, subgroup, variant,
  geography); ARCHITECTURE.md states that source facets may ride beside
  it when the ingester names them.

Rebased on main (#227); the generated feeds are main's until the rebuild.

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

This branch was successfully deployed

1 active deployment
Preview — 9a4d2ca0 Deployed Oct 25, 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