Skip to content

Calculate UK-wide and nation reports on the local-area dataset, with their areas and withheld codes - #1068

Draft
anth-volk wants to merge 45 commits into
mainfrom
v2plan/c1-23-local-area-report-profile
Draft

anth-volk wants to merge 45 commits into
mainfrom
v2plan/c1-23-local-area-report-profile

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1067

A run of a UK-wide or nation report on the release's local-area dataset (the dataset UK constituency and local-authority regions use, microcosm_uk_2024_25_dense on .py 6.17.1) now calculates its full results, cliffs included when asked, together with its constituency and local-authority results and the codes of the areas disclosure control withholds, and stores constituency_impacts, local_authority_impacts and withheld_area_codes rows. It is an ordinary report run, started by run ID; no field marks it, its simulations' dataset does.

Deploy order: the Public API's withheld_area_codes table and its grants (PolicyEngine/policyengine-api#4305, revision 1e1e3f079f2c) must be live in production before sim's release. They are: #4305 merged on 2026-11-13 as ca4d396e, and the Public API's release run 43519970359 applied the revision and its grants, and verified them, in staging and then in production (2026-11-13 23:20 UTC). Without them, a run of such a report would fail at its completion's insert; no other report writes to that table. Nothing creates such reports yet: the simulation service's data_source option comes in a later PR, which reads this PR's manifest declaration.

What changes

  • UK worker (stage12_runtime/simulation.py, new stage12_runtime/local_area_datasets.py): it accepts a uk or country/* simulation whose provenance names the local-area dataset, which it refused before ("simulation provenance differs from the installed bundle"). It selects that dataset in place of the region's own, keeps the region's scoping (a nation's filter), and checks every area column the area results read as the dataset loads, before simulating. The rule is .py's routing rule (uk_area_regions_routed, routed_uk_area_dataset_name), the one the national aggregation's uk_area_breakdowns_omitted already uses. It calls none of the v1 executor's local-area code.
  • Output plan (output_planning.py): a UK-wide or nation simulation on that dataset also copies UK_AREA_DATASET_COLUMNS, whether planned for a report or alone.
  • Aggregation (aggregation.py, SimulationOutputBuilder.uk_withheld_area_codes): for a report on that dataset, the existing national aggregation stops leaving the constituency and local-authority results out, and adds each level's withheld codes from the same disclosure pass. Such a run fails if a level has neither published nor withheld areas, so a succeeded run always has rows for each level. Every other report's output, and its comparison with production, is unchanged.
  • Economy output (SingleYearMacroOutput): optional constituency_withheld_codes and local_authority_withheld_codes, left out of the JSON when null, so every other result is written exactly as before. The gateway's golden OpenAPI spec gains the two fields.
  • Stage 12 writes (report_run_results.py): the withheld_area_codes table model, in the Public API's shape, and one row per withheld code, keyed by the run and its two simulation runs, written in the completion's transaction. A result that lists none writes none, so national runs never touch the table.
  • v2 version manifest (stage12_manifest.py, new stage12_manifest_models.py, the publisher): the UK entry declares local_area_reports (input_version, dataset), the dataset by the same routing rule, so the simulation service, which can't read the release kind from the catalog, can tell whether such a report is possible. Readers released before the field forbid unknown fields, and publish_stage12_v2_manifest can run before Cloud Run traffic moves, so the field is written only under a new key, active-schema-2-with-local-area-reports, which current readers read first while active-schema-2 holds its copy; active-schema-2 and the schema version 1 copies leave it out. The publisher requires both schema version 2 keys. The manifest's models moved, unchanged apart from the new one, to stage12_manifest_models.py (re-exported), keeping both files under the length limit. The Stage 12 doc's manifest section describes the key, the rollback handling, and that deleting active-schema-2 before a rollback also stops readers using the new key.

The design decisions it follows

From the local-area design's decisions (recorded 2026-11-13 and 2026-11-14):

  • regions: uk and the four country/* nations, with the executor's routing rule deciding which releases have a local-area dataset (decision 3, reworded; item 12 lets nations take it);
  • the local-area report always uses the release's local-area dataset (decision 4); with data_source, it is an ordinary report on that dataset returning its full results (the data_source design, replacing decisions 6, 8, 9 and 12);
  • cliffs are calculated when asked (decision 5);
  • withheld codes go in a new withheld_area_codes table (decision 7);
  • Stage 12's existing national aggregation calculates the areas, with no separate module (decision 14);
  • the Wales staging check (decision 13) belongs to the simulation service's data_source change; the area check above lets it read only the run's status.

Environment variables and secrets

None added. What these runs read, all already attached to coordinate_report and run_single_simulation_uk in v2_app.py:

  • the Hugging Face token, from the Modal secret named by HF_MODAL_SECRET_NAME (required_keys declares it), in worker_secrets: the worker downloads the private local-area dataset with it;
  • GOOGLE_APPLICATION_CREDENTIALS_JSON, from the Modal secret stage12-evaluation-gcp-credentials, in worker_secrets: output files and the constituency lookup file. That secret declares no required_keys, so the key it holds is unverified;
  • MODAL_WORKER_DATABASE_URL, from policyengine-modal-worker-database (required_keys), in report_run_writer_secrets.

The manifest publisher reads no new variable; it reads only the packaged .py manifest, and its job still needs only MODAL_TOKEN_ID and MODAL_TOKEN_SECRET. No workflow changed.

Tests

Each new test was seen failing for the expected reason first; only new and modified tests were run, by node ID.

  • Contract: the manifest declaration under its own key, never seen by older readers, ignored after a rollback, only on an economy-capable UK entry, naming a bundle dataset; the region-dataset rollback step sending every reader to active-with-cache.

  • Executor: the worker accepting, selecting and checking the local-area dataset for uk and nations, refusing it on a release that doesn't route and at another revision, and over the installed .py bundle unpatched; the recognition rule; the plan's area columns and cliffs; the aggregation's areas, withheld codes and area check, for UK-wide, nation, constituency, local-authority and national-dataset reports; the publisher's declaration and its two required keys (including --verify-plan with active-schema-2 deleted).

  • Stage 12 persistence: withheld rows from a result, and, on PostgreSQL 17 built from the Public API at #4305's revision with its privileges applied, writing them as the Modal worker role beside the area rows, no second set on a repeated completion, and a failed withheld insert rolling the whole completion back.

  • The gateway's golden OpenAPI spec, regenerated for the two fields.

  • The economy output's schema tests: SingleYearMacroOutput's fields and its published OpenAPI schema are the current result keys plus the two withheld-code fields (LOCAL_AREA_WITHHELD_CODE_KEYS); dumped and built results still have the current keys alone.

libs/policyengine-simulation-persistence/public-api-commit now points at #4305's merge commit, ca4d396e, for the Public API schema job, which compares only policyengine-simulation-persistence's tables, so it doesn't check Stage 12's withheld_area_codes model. Since the previous pin the Public API added no other migration.

Not covered by CI: no CI job sets the REPORT_RUN_TEST_* URLs, so Stage 12's PostgreSQL tests skip there (as before this PR); the three new ones were run by hand.

Unverified

  • The coordinator's memory (2 CPU, 8 GiB) for a UK-wide report on the local-area dataset, which decodes both full output files and runs the national analysis and 200 disclosure replicates: needs a measured Modal or staging run.
  • The v2 UK image holds only the UK default dataset (policyengine bundle install installs one per country), so each new worker container downloads the local-area dataset first, about 90 s in production.
  • That the local-area dataset gives Northern Ireland households non-empty area codes, so a Northern Ireland report passes the area check: not checked against the dataset itself.

Reviews

Six read-only review rounds. Round 1 found one bug, fixed test-first: the documented rollback step (deleting active-schema-2 and active-with-region-datasets) didn't stop readers released with the new key from reading it. Rounds 2 to 6 found none; their code-quality findings are fixed. Round 6 covered the two CI test-expectation fixes and the pin.

🤖 Generated with Claude Code

anth-volk and others added 30 commits November 13, 2026 18:51
…et (failing)

The coordinator must plan a UK local-area report run's simulations with
only the geographic aggregate and no cliff impacts, and the UK worker must
recognise the dataset a bundle gives its constituency and local-authority
regions.

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

ReportOutputRequirements accepts LOCAL_AREA_AGGREGATES, the geographic
aggregate alone with no cliff impacts, beside the complete profile.
Stage12CountryBundle.local_area_dataset names the one dataset both UK area
region types load when it differs from the national one.

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

The plan names only the household columns disclosure control reads, the
same for every policy pair, so a local-area baseline run is reusable by
any report with that baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
resolve_local_area_output_plan gives both simulations one household plan:
the identifier, net income and weights, which the UK model writes by
default, and the area columns copied from the local-area dataset whatever
the region. It adds no cliff, labour-supply or budget variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UK worker refuses a UK-wide or nation simulation whose provenance
names the dataset the bundle gives constituency and local-authority
regions, which a local-area report run's simulations calculate on.

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

The UK worker accepts a UK or nation simulation whose provenance names the
dataset the bundle gives constituency and local-authority regions, selects
that dataset in place of the region's own, keeps the region's scoping, and
checks every column the area outputs read as the dataset loads. It reuses
only the runtime helpers Stage 12 already calls.

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

The simulation service refuses a local-area breakdown before writing
anything unless the default v2 worker declares that its coordinator runs
local-area report runs. Readers released before the declaration forbid
unknown fields and can read the manifest before the entry is promoted, so
it goes under a new key that readers with it read first, while
active-schema-2 keeps the manifest without it.

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

V2WorkerVersion gains local_area_reports, the LOCAL_AREA_REPORTS_VERSION of
the UK local-area report runs a worker's coordinator runs, left out of the
JSON when None so other workers' documents and digests stay the same. The
publisher sets it. It is written only under the new
active-schema-2-with-local-area-reports key, which readers with it read
first; active-schema-2 keeps the manifest without it, and a copy left
behind by an older publisher after a rollback is no longer read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd worker (failing)

The simulation service can't tell from the catalog whether a release's UK
national runs leave the area results out, so the UK country entry of the
v2 manifest declares the local-area dataset by the rule Stage 12's
national aggregation uses, and the UK worker accepts that dataset by the
same rule.

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

The UK country entry's LocalAreaReports (input_version, dataset) replaces
the worker-wide field. The publisher sets dataset by uk_area_regions_routed
and routed_uk_area_dataset_name, the rule Stage 12's national aggregation
uses, and the UK worker accepts a UK-wide or nation simulation on the
local-area dataset by the same rule, so the simulation service creates an
area part exactly when the national run leaves the areas out. The bundle
helper local_area_dataset, which compared region datasets instead, goes.

The manifest's models move, unchanged apart from the new one, to
stage12_manifest_models, which stage12_manifest re-exports, keeping it
under its line limit.

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

A local-area report is now an ordinary report on the local-area dataset,
linked to its national report by key, with no marker in its input, so the
manifest declaration and the Stage 12 doc describe the runs the coordinator
accepts that way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…a dataset (failing)

The local-area report is an ordinary report on the local-area dataset, so
the coordinator recognises its runs by their simulations' dataset and
region, not by a marker in the input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
is_local_area_report decides by the report's region and its simulations'
dataset, by the routing rule the UK worker and the national aggregation
use, so the coordinator can calculate the area results alone for any such
report, however the simulation service found or created it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C1 · 21's decision 7 puts the codes disclosure control withholds in a new
withheld_area_codes table, keyed like the other comparison tables. A
local-area run stores its constituency and local-authority rows, one
withheld-code row per withheld area, and its data version, and nothing
else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
local_area_result validates a LocalAreaBreakdownOutput, refusing one that
doesn't list each level's withheld codes, and returns its constituency and
local-authority rows (through the national result's area rows, now
area_record_rows), one withheld_area_codes row per withheld area, and the
run's data version. ReportRunRecorder.complete_local_areas writes them in
the completion's transaction under the call's claim. The table model
follows C1 · 21's decision 7; its column types are checked against C1 · 22's
revision once it exists.

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

C1 · 26's stored output plans copy the area columns for a UK-wide or nation
simulation on the local-area dataset, by the same rule the worker and the
coordinator use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
calculates_on_local_area_dataset is the rule is_local_area_report and the
worker's dataset check share; C1 · 26's stored output plans use it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rea dataset (failing)

A simulation run the simulation service starts on its own may later be
named by a local-area report, which reads the area columns. Today the
plan copies them only for constituency and local-authority regions.

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

The output plan copies UK_AREA_DATASET_COLUMNS for such a simulation too,
by C1 · 23's rule (calculates_on_local_area_dataset), whether it is
planned for a report or alone, so its run's file holds what a local-area
report reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the data_source design a report on the local-area dataset returns its
full results, cliffs included when asked, and its areas, so the area-only
output plan (resolve_local_area_output_plan) and the contract's
local-area aggregate profile go. The output plan's area columns come from
C1 · 26's change, taken here; a test checks such a report plans every
aggregate, its cliffs and the area columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A UK-wide report on the local-area dataset returns a national result, so
its withheld area codes travel in that result and become
withheld_area_codes rows when it is stored. The area-only result's tests
go with the area-only output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SingleYearMacroOutput gains constituency_withheld_codes and
local_authority_withheld_codes, left out of the JSON when null so every
other result is written and compared as before, and report_run_result
writes one withheld_area_codes row per code. A UK-wide report on the
local-area dataset returns a national result, so the area-only result
(local_area_result, complete_local_areas) goes. The gateway's golden
OpenAPI spec gains the two fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the data_source design a report on the local-area dataset returns its
full results as well as its area results, so the manifest declaration, the
recognition rule's docstring and the Stage 12 doc say so.

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

C1 · 21's decision 14: Stage 12's national aggregation stops leaving the
areas out of a report on the local-area dataset and reads the withheld
codes from the same disclosure pass. A UK-wide report on the national
dataset still leaves them out, and an area report lists no withheld codes.

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

C1 · 21's decision 14: build_aggregate_report stops leaving the
constituency and local-authority results out of a report on the
local-area dataset (is_local_area_report), and adds each level's withheld
codes from the same disclosure pass, through the new
SimulationOutputBuilder.uk_withheld_area_codes. Other reports, and their
comparison with production, are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The report output planning section says how a UK-wide or nation report on
the local-area dataset loads it, plans its area columns, and keeps its
areas and withheld codes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: deleting active-schema-2 and active-with-region-datasets
before deploying code released before region datasets must stop readers
released with the local-area key from reading it, and the publisher must
require active-schema-2 again, since those readers use the new key only
while it holds its copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1: the documented step before rolling back to code released
before region datasets deletes active-schema-2 and
active-with-region-datasets, but readers released with the local-area key
counted a missing active-schema-2 as matching and kept reading the new key,
with its region datasets. The new key's document is now current only while
active-schema-2 holds its copy, and the publisher requires both keys. The
Stage 12 doc says so, and that a rollback drops local_area_reports from
versions until each is published again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SingleYearMacroOutput's description, which the gateway's OpenAPI spec
publishes, no longer names Stage 12 and covers nation reports; the
uk_area_breakdowns_omitted and SimulationOutputBuilder docstrings say that
Stage 12 keeps the areas for a report on the local-area dataset; and the
aggregation imports is_local_area_report at module level, since the UK
metadata module is already loaded with the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The aggregation tests cover a nation report on the local-area dataset, and
a new test stores a complete local-area aggregate through
AggregateReportArtifactPayload and report_run_result, with its withheld
rows. Test names now say what they check, and the worker tests' bundle has
no nation region dataset, as .py 6.17.1's has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 15 commits November 14, 2026 00:35
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2: no test depended on the publisher requiring
active-schema-2. With it deleted, current readers stop using the
local-area report key and read an older copy, so --verify-plan must refuse
the store; without the requirement it accepted it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 2: every worker test replaced the bundle, the routing and the
dataset selection. This one uses the installed .py bundle and routing, so a
release that renames or moves the local-area dataset, or gives it another
URI in dataset selection, fails here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 doc's older manifest paragraphs say which key current readers
use, now that the local-area report key comes first, and neither the doc
nor is_local_area_report's docstring says a request can already ask for
such a report: the simulation service names the dataset, and no field
marks the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 doc says a manifest with a country that isn't economy-capable
goes under both schema version 2 keys, and no longer says the simulation
service already uses the local-area declaration. The installed-release
worker test calls the worker's own provenance check on the selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Against a database built from C1 · 22's revision of the Public API
(withheld_area_codes, 1e1e3f079f2c): completing a run whose result lists
withheld codes, as the Modal worker role, writes one row per code, naming
the run's two simulation runs, beside its area rows, and a repeated
completion writes no second set. Seen failing with the rows left out. The
table is reflected in these tests alone, so the others still run on a
database built before it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new test makes the withheld insert itself fail (a code listed twice) and
checks the whole completion rolls back: the run stays running, with no
area or withheld rows. The local-area tests also check the
local-authority rows, share one fixture (uk_output_with_withheld_codes),
and the package README says which Public API revision they need.

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

A run of a report on the local-area dataset succeeds today even when a
level has neither published nor withheld areas. Failing it instead means a
succeeded run proves each level's rows exist, which the staging check
needs, since it reads only the run's status.

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

build_aggregate_report refuses, for a report on the local-area dataset, a
level with neither published nor withheld areas, which a UK-wide or nation
report always has. A succeeded run then proves each level's area or
withheld-code rows exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A test covers a level whose areas are all published, which a check
ignoring the published areas would fail; the stand-in builder takes its
published and withheld areas through monkeypatch; and the aggregation's
docstring, the Stage 12 doc and the changelog fragment say a local-area
run fails when a level has no estimated area.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI: a manifest schema version 1 can't describe is now written under
active-schema-2-with-local-area-reports as well as active-schema-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ca4d396e merges PolicyEngine/policyengine-api#4305, whose revision
1e1e3f079f2c adds the withheld_area_codes table that Stage 12 writes a
local-area report run's withheld codes to. Since the previous pin it adds
no other migration and changes no table sim's persistence models describe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SingleYearMacroOutput's fields and its published OpenAPI schema now hold
constituency_withheld_codes and local_authority_withheld_codes, which only
a UK-wide or nation report on the local-area dataset lists. The schema
tests expect the current result keys plus those two (LOCAL_AREA_REPORT_KEYS);
the tests of dumped and built results still expect the current keys alone,
since every other result leaves the two out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The persistence README says the pin may move to a later merged commit that
alters no modelled table, and that the job then still checks only this
package's tables. The fixture's set of the two withheld-code fields is
LOCAL_AREA_WITHHELD_CODE_KEYS, since it holds only those two keys.

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

This branch has not been deployed

No deployments
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.

Stage 12: local-area results for UK-wide and nation reports on the local-area dataset

1 participant