Skip to content

Serve UK constituency and local-authority results from the routed local-area dataset - #881

Merged
anth-volk merged 32 commits into
mainfrom
uk-728-area-breakdowns-microcosm
Oct 23, 2026
Merged

anth-volk merged 32 commits into
mainfrom
uk-728-area-breakdowns-microcosm

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #880

Part of #728. This makes the executor serve policyengine.py 6.10.0 (PolicyEngine/policyengine.py#685), which makes the Microcosm national dataset the UK default and routes constituency and local-authority regions to the Microcosm local-area dataset. UK-wide and nation-level reports run on the national file alone. Their constituency and local-authority results come from a separate run, requested on demand with breakdown: "local_areas", on the local-area file alone.

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Summary

After this PR, on a release built with .py 6.10.0:

  • UK-wide and nation-level requests (region absent, uk or country/<nation>) load only microcosm_uk_2024_25 and build one baseline and reform. They compute no constituency or local-authority results: both fields are null, and the job status body omits them. This removes the second baseline and reform run on the local-area file from every such request (about 70-90 s and 12 GiB each), from every UK budget-window year and from the UK release sentinel.
  • Local-area breakdowns. A UK-wide or nation-level request with breakdown: "local_areas" loads only microcosm_uk_2024_25_dense and builds one baseline and reform there with the region's filter. Both area levels then come from one disclosure-controlled release (Use disclosure-controlled UK geography outputs for constituency and local authority impacts #860), and the job returns a LocalAreaBreakdownOutput.
  • Constituency and local-authority requests are unchanged: they run on the local-area file, as María's commits below set up.
  • Older releases. Each deployment rebuilds only the worker app for the current .py version (the deploy_executor job redeploys policyengine-simulation-py<current version> from main); apps for older .py versions are not rebuilt, and they ignore fields they do not know. The gateway therefore sends a breakdown only to a worker whose routing bundle manifest declares the new capability, and refuses it with 400 for any other.
  • Which bundles count as routing UK area regions. The manifest's regional_release_kind must be local_area, and both area region types must route to one certified dataset. A bundle of kind area_coded_national, which .py certifies without registering area regions, or one that routes the two types to different datasets, counts as not routed and declares no capability.
  • Dataset checks before simulating. A routed local-area dataset must carry all four household columns the area outputs read (constituency_code_oa, la_code_oa, region_code_oa, source_household_id) as it loads, for breakdown, constituency and local-authority runs alike, in the runtime and in the Stage 12 worker.
  • Dataset checks before merge. The executor image smoke and the Stage 12 static-files smoke check the same four columns on the downloaded routed file, reading only the household table's column names, so the PR image smoke checks the real file.

Contract

  • SimulationRequest.breakdown: Literal["local_areas"] | None = None. Accepted only for UK requests whose region is absent, uk, country/england, country/scotland, country/wales or country/northern_ireland (the nations .py 6.10.0 registers; an executor test checks the list against the installed registry). Refused with 422 for other countries, any other region (constituency, local-authority, bare-name, country/ni or an unknown nation), include_cliffs: true, segmented: true, a region_group or any other value. include_cliffs: false stays accepted, because the Public API always sends it. BudgetWindowBatchRequest does not take the field.
  • LocalAreaBreakdownOutput (extra="forbid"): kind: "local_areas", model_version, policyengine_version, dataset, data_version, constituency_impact: list[ConstituencyImpactRecord] and local_authority_impact: list[LocalAuthorityImpactRecord]. Each list takes only its own level's records, and either may be empty.
  • JobStatusResponse.result is SingleYearMacroOutput | LocalAreaBreakdownOutput | None. Both models forbid extra fields, so a payload validates as only one of them.
  • Routing bundle manifest key local_area_breakdown, version 1, written by the routing publisher only for apps whose bundle routes UK area regions as described above. The gateway refuses breakdown with 400, before spawning, for any route whose manifest lacks it, including routes pinned to an older version and routes from the legacy version dicts. The error names the .py version.
  • The entry starts no Stage 12 comparison for a breakdown request (skip reason unsupported_options).

The gateway OpenAPI golden and the generated client are regenerated.

Commits

María's commits (unchanged):

  • 5ad72b0f Build routed UK area results from the local-area dataset
  • 6aad6520 Check the routed area dataset in the image smokes
  • c2728bce Copy no area codes into Stage 12 for a routed UK national report
  • 24690a27 Record the routed UK area changes in the changelog

Added on top:

SHA Commit Kind
a96c72c2 Merge origin/main into uk-728-area-breakdowns-microcosm merge, no conflicts
7dad9206 Load the routed UK area pair through main's dataset and span helpers red-green
acc1d2bf Use disclosure-controlled UK geography outputs for constituencies and local authorities cherry-pick of #860's 3c99e59d (Pavel), adapted for this PR
897e4b56 Withhold the inner areas that sum to a withheld UK area cherry-pick of #860's 8fb0b788 (Pavel)
0e132423 Type the UK area pair counts that the containment helper reads Pyright fix for the previous commit
73dfd895 chore(deps): update policyengine to 6.10.0 cherry-pick of #883's 2e98847d (github-actions)
0ec2f4d4 Require the disclosure columns on the routed area dataset before simulating red-green
3f031fd2 Add breakdown=local_areas to the simulation request and its result model red-green
f14cc7ba Run a local-area breakdown on the local-area dataset alone red-green
841fc732 Stop building the area pair in UK-wide and nation runs on routed bundles red-green
362a904c Declare the local-area breakdown capability in the routing manifest red-green
ef6b7eca Refuse breakdown requests for workers that do not declare serving them red-green
ad9c35da Check UK local-area breakdowns and area-free national results in beta red-green (local helper tests)
3cdc8149 Document UK local-area breakdowns and record them in the changelog docs
895d20dc Remove the UK weight-matrix region fallback red-green

Added after the first review:

SHA Commit Kind
17c73b1e Count UK area regions as routed only to one local-area release dataset red-green
5ecf8d47 Check every area-output column before a routed area run simulates red-green
f2b7685e Accept only the UK nations .py registers in a breakdown request red-green
1fd5c004 Type each breakdown level's records and the area pair precisely red-green (contract) and typing
dad34498 Build local-area breakdowns in their own builder refactor, no behaviour change
f77a8bee Test the installed bundle's routing and a version-pinned refusal tests of existing behaviour
2e3c1bf3 Correct the changelog and gateway README for the review's points docs
ff0e3d97 Name the breakdown builder in the microdata-weights guidance docs

Added after the second review:

SHA Commit Kind
1eab140d Check every area-output column of the routed file in the image smokes red-green
64fdfed7 Share the output builders' span and version helpers refactor, no behaviour change
b34a7052 Type the breakdown builder's country module and drop an unused field typing, no behaviour change
dbf75b34 Test a Scotland breakdown on the installed bundle and the check fallback tests of existing behaviour
9503823d Record the image smokes' column check in the changelog docs

Correction to 895d20dc's commit message: it says only the gateway and entry are redeployed from main. The deploy_executor job also redeploys the app for the current .py version from main on each deployment. The removal is still safe, because that app is now 6.10.0, which routes UK area regions; apps for older versions are not rebuilt.

The cherry-picks use git cherry-pick -x, so each keeps its author and names its source commit. acc1d2bf's message has an "Adapted for #881" paragraph. Its disclosure release reads the pair the area breakdowns use, #881's builder tests patch compute_uk_geography_disclosure_output, and the planning test expects the four household columns. #860 and #883 are still open; this PR now contains their commits.

Red-green

Each commit marked red-green was written tests first. The new tests were run and failed for the expected reason before the code was written, and the tests and the code were committed together. Each commit message records how its tests failed. For example:

  • the request tests failed with extra_forbidden where they expect value_error or literal_error;
  • gateway polling of a breakdown result returned 500;
  • invalid breakdown requests reached the national path (DID NOT RAISE);
  • a routed national run loaded both datasets;
  • the routing manifest had no local_area_breakdown (KeyError);
  • the gateway returned 200 and spawned the job for workers without the capability;
  • after the first review: an area_coded_national bundle or one routing the two area types to different datasets counted as routed (assert True is False) and declared the capability; constituency and local-authority runs on a dense file missing a column built both simulations (DID NOT RAISE, and in Stage 12 "simulated a dataset it cannot use"); country/ni and country/atlantis passed the contract (DID NOT RAISE ValidationError); a local-authority record validated in the constituency list (DID NOT RAISE ValidationError);
  • after the second review: both image smokes passed a routed file missing constituency_code_oa, region_code_oa or source_household_id (DID NOT RAISE <class 'ValueError'>), and raised a generic error that does not name the file for a missing la_code_oa.

A few new tests pinned behaviour that already held, and passed before the change; their commit messages say which. Examples are the Stage 12 adapter skipping an unknown field, the non-default-dataset rule for deterministic baseline ids, f77a8bee's two tests (the installed bundle's real routing and revision, and a refusal pinned by UK model version), and dbf75b34's tests (a country/scotland breakdown resolved against the installed bundle, and the other branch of detect_uk_run_dataset_metadata).

A merge and cherry-picks cannot be written test-first. They were checked by running the full suites, as below.

Testing

On the final tree, with Python 3.13 and the pinned ruff 0.15.22:

  • Unit tests, each in its own locked environment (uv run --frozen pytest tests/):

    • executor: 1862 passed, 22 skipped;
    • contract: 322 passed;
    • gateway: 268 passed;
    • entry: 668 passed;
    • observability: 35 passed;
    • Stage 12 persistence: 40 passed;
    • fastapi: 14 passed.
  • make check passes. It regenerates the client, runs ruff format and check, and runs Pyright in every package. The executor's Pyright errors went from 77 to 68: Use disclosure-controlled UK geography outputs for constituency and local authority impacts #860 removed 6 and the weight-matrix removal 3, and no new error is recorded.

  • Other checks:

    • python scripts/run_quality_guards.py passes.
    • python -m scripts.rules render leaves no diff.
    • uv lock --check passes for the executor.
  • Generated client: ./scripts/generate-clients.sh, then the policyengine-apis-integ tests that need no services (24 passed). test_ping needs the docker-compose services and was not run.

  • On the real 6.10.0 bundle, without loading data, the breakdown path resolves uk, country/wales and country/northern_ireland to microcosm_uk_2024_25_dense with its revision microcosm-uk-2024-25-dense-20261019T000432Z-ee5c2dbe, and refuses constituency/Aldershot. A unit test now checks the routing and the revision against the installed bundle.

  • OpenAPI golden regenerated in 3f031fd2 and again in 1fd5c004 (the two area lists now reference their record schemas); docs/generated/rules.md renders with no diff.

  • Not run locally:

    • the Docker builds, the Modal image smoke and the local integration job;
    • the beta integration tests: test_calculate_uk_local_area_breakdown is new, and test_calculate_uk_model gains an assertion;
    • an end-to-end breakdown on the private data.

    CI and the beta deploy run them.

Environment variables

None of the changed code reads a new environment variable.

  • Routing publisher (update_version_registry.py, changed in 362a904c; its condition tightened in 17c73b1e). The new key comes from uk_area_regions_routed(), which reads regional_release_kind, region_datasets and datasets from the bundle manifest packaged inside the installed policyengine package (policyengine/data/bundle/manifest.json), with no network access and no environment variable.
    • The publisher's existing variables are unchanged. MODAL_TOKEN_ID and MODAL_TOKEN_SECRET are GitHub Actions repository secrets; both names are listed in the repository's secrets. simulation-deploy.yml passes them to simulation-deploy.reusable.yml with secrets: inherit, and the plan, record and publish steps set them in env:.
    • MODAL_ENVIRONMENT, APP_NAME, POLICYENGINE_VERSION, POLICYENGINE_US_VERSION, POLICYENGINE_UK_VERSION, FORCE_LATEST and RELEASE_ENVIRONMENT come from workflow inputs and job outputs.
    • GITHUB_WORKFLOW_REF, GITHUB_ACTIONS, GITHUB_REPOSITORY, GITHUB_RUN_ID and GITHUB_RUN_ATTEMPT are set by GitHub.
  • Executor. A breakdown run downloads the private local-area file through the existing run_simulation Modal function. That function already attaches the pe-uk-private-hf-read-token Modal secret, with required key HUGGING_FACE_TOKEN, and gcp-credentials. Constituency and local-authority runs on 6.10.0 already use the same download. I confirmed the attachment in code only; I did not check that the Modal secret exists in Modal.
  • Gateway and entry read nothing new.

Code-owner review

These commits change files under rules/, which need code-owner approval (sim.process.rule-machinery-code-owned):

The commits added after the two reviews change nothing under rules/; the executor's Pyright errors stay at 68.

Rollout

  1. Merge and deploy this PR. That creates policyengine-simulation-py6-10-0 with the local_area_breakdown capability, and moves latest to 6.10.0. The Public API still pins 6.7.5, so its users see no change. Unpinned clients get national UK results without area data.
  2. The app-v2 change can merge on its own. Outputs from 6.7.5 carry area data, so its Compute buttons do not appear yet.
  3. The Public API change sends breakdown and moves to 6.10.0. It needs this PR deployed first.

Risks and follow-ups

  • The 6.10.0 executor is frozen once main's lock moves past it, so later fixes to the breakdown code reach only newer versions.
  • An entry rollback to an older Cloud Run revision would return 422 for breakdown.
  • The local-area file (about 720 MB) is downloaded on every cold container. Measure this in beta, and consider baking it into the image.
  • Disclosure control may withhold many areas on the local-area file, or publish nothing for a level. The cost of its 200 bootstrap replicates on that file has not been measured. The beta breakdown test fails if either level is empty.
  • country/ni, the Public API's spelling, is refused with 422; .py names it country/northern_ireland. The Public API change maps it before sending.
  • data_version of a breakdown falls back to the release's data version if the manifest records no revision for the local-area dataset, as the macro output does. 6.10.0 records one.
  • data_version on single-area UK runs still names the national release (_selected_dataset_revision is US-only).
  • No Stage 12 local-area report exists, so the shadow comparison does not cover breakdown numbers.
  • The executor's synchronous FastAPI surface (compat_models.SimulationOptions, used by docker-compose) does not take breakdown.

🤖 Generated with Claude Code

Maria Juaristi and others added 4 commits October 21, 2026 18:18
When the policyengine.py bundle routes UK constituency and local-authority
regions to a local-area dataset and the national default carries no area
codes (policyengine.py 6.10.0), a national or nation-level run builds its
two area breakdowns from a second baseline and reform run: the same
policies and nation filter, on the routed dataset. That dataset must carry
constituency_code_oa and la_code_oa, and its authority codes must match one
packaged boundary version. The national figures still come from the run's
own pair, and the builder computes the second pair when the first
breakdown needs it.

Once routed, constituency and local-authority regions resolve by name or by
GSS code in any case to the row-filter regions in policyengine.py's
registry, so policyengine-api's constituency/{name} and
local_authority/{name} requests keep working, and no weight-matrix region is
built for them. The 17 districts replaced in April 2023 are unsupported.

Nothing changes on policyengine.py 6.7.5, which routes no area regions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The executor and Stage 12 image smokes read the installed UK default's
local-authority codes. When the default carries none and the bundle routes
area regions to another dataset, they now download that dataset next to
the default, check it against the bundle digest, and read its codes
instead. Both smoke functions mount the Hugging Face credential for that
download, and the Stage 12 smoke asks for 2 GiB instead of 512 MiB to read
the routed file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Stage 12 UK national or nation-level report whose bundle routes area
regions to another dataset plans no constituency_code_oa or la_code_oa
columns, since its national dataset may carry none, and its aggregate
leaves the constituency and local-authority breakdowns out. Stage 12 has no
second run on the routed dataset yet, so its shadow comparison reports the
two breakdowns as missing for such reports. The planning tests that
describe unrouted reports now pin routing off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@juaristi22
juaristi22 requested a review from anth-volk October 22, 2026 09:13
anth-volk and others added 2 commits October 22, 2026 22:15
Brings in main up to 1f59a56 (#885 and the rules, Pyright and
locked-install work before it). Git merged every file without conflicts,
including changelog_entry.yaml, which keeps both sides' entries.

Checked after the merge: the contract (274 passed), gateway (261 passed)
and entry (662 passed) suites and the quality guards pass. Two known
failures remain, both fixed by the next commit:

- The executor suite has 2 failures out of 1789, the two routed-national
  tests in test_simulation_output_builder.py. main's run now passes
  data_folder= to _load_dataset (for the baked state folders), and the
  tests' fake does not take that keyword:
  TypeError: ... got an unexpected keyword argument 'data_folder'.
- The executor's Pyright check reports five reportPossiblyUnbound
  errors in _uk_area_simulations, which opens its spans with
  runtime.span(...) instead of scopes.span(runtime, ...).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After the merge of main, two routed-national tests and the executor's
Pyright check failed:

- main's run passes data_folder= to _load_dataset, and the routed tests'
  fake did not take it (TypeError: unexpected keyword argument
  'data_folder'). The fake is now a named function that takes
  data_folder=None.
- _uk_area_simulations opened its three spans with runtime.span(...),
  which Pyright reads as possibly suppressing exceptions, so it reported
  selection and dataset as possibly unbound. The spans now open with
  scopes.span(runtime, ...), as docs/engineering/skills/type-checking.md
  requires; the span names and attributes are unchanged.

The executor suite passes (1789 passed), and Pyright is back to the 77
errors its baseline lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PavelMakarchuk and others added 6 commits October 22, 2026 22:25
… local authorities

The executor built UK constituency and local-authority results with
policyengine's deprecated compute_uk_constituency_impacts and
compute_longwise_uk_geography_impacts. They apply no minimum sample, so a
small area with negative-income households shows an exploding relative
change (Cardiff West: -£192 average, +1372% relative), and their output is
not disclosure-controlled for licensed survey microdata.

Both levels now go through compute_uk_geography_disclosure in one release,
nested in regions and the nation, with the constituency/local-authority
overlap acknowledged. Blocking findings are resolved by withholding the
suggested published cells; region and national reference cells are never
withheld, and a release that cannot be resolved publishes no area. The
share families, which the output does not publish, are withheld up front.

Contract: existing fields keep their types. A withheld area, or one whose
relative change is undefined, is left out of the list. Published records
gain optional disclosure_tier and 90% interval fields. An empty list is
now valid. Stage 12 keeps region_code_oa and source_household_id.

The local-authority breakdown no longer strips weights, so the last
recorded weight-stripping exception is removed and its baseline is 0.

Adapted for #881: the builder keeps #881's uk_area_simulations and
omit_uk_area_breakdowns. _uk_geography_disclosure_output() computes the
release over the pair _uk_area_breakdown_inputs() returns (the routed
local-area pair when given, otherwise the run's own pair), the
local-authority impact takes its boundary metadata from
_uk_area_breakdown_inputs()[2], and both builders return None before
computing anything when the breakdowns are omitted. #881's builder tests
now patch compute_uk_geography_disclosure_output(country, baseline,
reform, *, year), test_omitted_uk_area_breakdowns_compute_nothing fails
if it is called, and #881's planning test expects the four household
columns. The changelog keeps both sides' entries. #860 also removes six
executor Pyright errors in simulation_output_common.py and
simulation_output_geographic.py, so the executor baseline shrinks from 77
to 71 entries, and docs/generated/rules.md is rendered again.

Fixes #859

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 3c99e59)
A containment finding without a suggested cell names only the outer area.
When that area is withheld and the public areas of the other level inside
it add up to it (Orkney and Shetland is Orkney plus Shetland), the loop
withheld the outer cell again, made no progress, and published no area at
all. It now withholds the inner areas that lie wholly inside the outer
one, read from the household area pairs as the release reads them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8fb0b78)
The previous commit's _uk_areas_inside unpacks each key of a two-column
groupby count as a pair of area codes. pandas types those keys as
Hashable, so the executor's Pyright check reported a new error
("Hashable" is not iterable). The key is now checked to be a tuple
before it is unpacked; a two-column groupby always gives one, so results
do not change.

Pyright is back to the 71 errors its baseline lists, and the executor
suite passes (1804 passed).

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

#860's disclosure-controlled area outputs read region_code_oa and
source_household_id as well as the two area codes. The check a routed
local-area dataset gets as it loads (detect_uk_area_dataset_metadata)
required only the area codes, so a dataset without the other two would
simulate both pairs and fail only when the outputs were built.

UK_AREA_DATASET_COLUMNS in uk_local_authority_metadata.py now lists the
four household columns those outputs read. The load check requires each
of them and names the dataset and the missing column, and Stage 12's
UK_GEOGRAPHIC_DATASET_VARIABLES copies the same tuple, so the two cannot
drift apart.

Tests, written first:
- test_routed_area_dataset_needs_every_column_area_outputs_read, over
  the four columns (replacing the two-column test). Before the change,
  the region_code_oa and source_household_id cases failed with
  "DID NOT RAISE <class 'ValueError'>".
- test_uk_geographic_plan_copies_the_routed_dataset_columns. Before the
  change it failed with AttributeError: module ... has no attribute
  'UK_AREA_DATASET_COLUMNS'.
The routed-national runtime test's local-area fixture gains the two
columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A UK report's constituency and local-authority results will come from a
separate run, requested on demand, instead of from every national run.
This adds the contract for that run:

- SimulationRequest gains breakdown: Literal["local_areas"] | None. It is
  accepted only for a UK request whose region is absent, uk or
  country/<nation>, and never with include_cliffs=true, segmented=true or
  a region_group. include_cliffs=false stays accepted, because the
  Public API always sends it. The rules live in
  local_area_breakdown_error() in the new local_area_breakdown module,
  so the worker can apply the same ones. BudgetWindowBatchRequest does
  not take the field.
- LocalAreaBreakdownOutput (extra="forbid") is the result of such a run:
  kind "local_areas", model_version, policyengine_version, dataset,
  data_version, and the constituency_impact and local_authority_impact
  lists, which may be empty.
- JobStatusResponse.result is SingleYearMacroOutput |
  LocalAreaBreakdownOutput | None. Both models forbid extra fields, so a
  payload validates as only one of them.
- local_area_breakdown also defines the routing manifest capability
  (key local_area_breakdown, version 1) and
  supported_local_area_breakdown_capability(), copied from job_progress.

The gateway OpenAPI golden is regenerated; its diff adds the field, the
result model and the result union.

Tests, written first, and how they failed before the change:
- contract: the request tests failed with extra_forbidden where they
  expect value_error or literal_error, or with AttributeError: no
  attribute 'breakdown'; test_local_area_breakdown.py failed to import
  (No module named 'policyengine_simulation_contract.local_area_breakdown').
- gateway: polling a complete breakdown result returned 500 (the result
  failed SingleYearMacroOutput validation); it now returns 200.
- entry: forwarding a breakdown got 422; the constituency-region refusal
  was 422 extra_forbidden instead of value_error; the OpenAPI test failed
  with KeyError: 'breakdown'.
- executor: the published result schema had no LocalAreaBreakdownOutput.
The entry tests that national submissions carry no breakdown and that
the Stage 12 adapter skips a breakdown (unsupported_options) but ignores
an absent one passed before the change: the adapter already skips any
field it does not know. They pin that behaviour. The budget-window
rejection test passes before and after for the same reason.

Suites: contract 312, gateway 262, entry 668 and executor 1808 passed;
make check (ruff, Pyright in every package, after regenerating the
client) passes, and the generated-client tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 7 commits October 22, 2026 23:12
A request with breakdown=local_areas now takes its own path through the
executor (_run_local_area_breakdown_core), which _run_simulation_impl_core
branches to before loading anything:

- It checks the request again with the contract's
  local_area_breakdown_error() (UK only; region absent, uk or
  country/<nation>; no include_cliffs=true, segmented=true or
  region_group), refuses any other breakdown value, and refuses a bundle
  that does not route UK area regions to a local-area dataset. Each of
  these raises ValueError before the country module or any dataset loads.
- It reuses _uk_area_simulations to load only the routed local-area
  dataset (microcosm_uk_2024_25_dense on .py 6.10.0) and build one
  baseline and reform with the request's policies and nation filter, and
  _report_calculation_start for the baseline and reform stages. The
  national file is never loaded, and the dataset_load span carries
  dataset_role=uk_area.
- The builder's new serialize_local_area_breakdown() computes the
  baseline, then the reform, enters the outputs stage, and builds the two
  existing area breakdowns from one disclosure release under
  simulation_output_build. It returns a LocalAreaBreakdownOutput with the
  model and policyengine.py versions, the dataset name and its revision.
  No national impact analysis runs, and no new stage name is needed.
- _selected_dataset_revision is split so _manifest_dataset_revision(country,
  name) reads the local-area dataset's revision from the release manifest.
- UKAreaSimulations also carries the loaded dataset and its name, and
  the start-of-run logging moved into _log_simulation_start to keep
  _run_simulation_impl_core within ruff's statement limit.

Tests, written first (the routed-run helper is generalised into
_run_routed_uk(monkeypatch, *, params, local_area_dataset)), and how they
failed before the change:
- the breakdown run test: AttributeError, no _manifest_dataset_revision;
  the UK-wide run loaded the national dataset and then the dense one;
- the invalid-request cases: DID NOT RAISE ValueError (the US case raised
  SPMInputError from the national path instead);
- the unrouted bundle: the national path's missing la_code_oa error, not
  the breakdown refusal;
- the three builder tests: TypeError, unexpected keyword
  'resolved_dataset_name'.
The test that a breakdown needs all four routed-dataset columns, and the
pin in test_baseline_artifacts.py that a non-default UK dataset gets no
deterministic baseline id, passed before the change: the load check and
the id rule already hold. They pin both for the breakdown path.

Suites: executor 1826, contract 312, gateway 262 and entry 668 passed;
Pyright stays at the executor baseline's 71 errors; ruff and the quality
guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Until now a UK-wide or nation-level run on a bundle that routes area
regions to the local-area dataset loaded that dataset as well and built
a second baseline and reform there, only for the constituency and
local-authority results: about 70-90 s and 12 GiB more per request, in
every budget-window year and in the UK release sentinel. Those results
now come from the separate breakdown=local_areas run, so:

- uk_area_breakdowns_omitted(country, region) in
  uk_local_authority_metadata.py is True for a UK-wide or nation run on a
  routing bundle, and False for area regions, unrouted bundles and other
  countries. The runtime passes it to the builder as
  omit_uk_area_breakdowns, and Stage 12's aggregation uses the same
  helper, so the runtime and the Stage 12 shadow results agree again.
- The runtime no longer calls _uk_area_simulations for national runs,
  and the builder loses uk_area_simulations and _uk_area_breakdown_inputs:
  its disclosure release reads its own baseline and reform. Such runs
  return null for constituency_impact and local_authority_impact.
- UKAreaSimulations, now used only by the breakdown run, moves to
  simulation_runtime.py.

Tests, written first, and how they failed before the change:
- test_area_breakdowns_are_omitted_only_from_routed_uk_wide_and_nation_runs
  and the Stage 12 omission test's new check: AttributeError, no
  uk_area_breakdowns_omitted.
- test_a_routed_uk_wide_or_nation_run_builds_no_area_pair (uk and
  country/wales): the run loaded the national and the local-area
  datasets instead of one.
#881's test that national runs build the area pair is deleted, with its
companion that checked the local-area dataset's columns during a
national run (the breakdown run's tests cover that check), and so is the
builder test of the routed pair. The expected builder arguments now
carry omit_uk_area_breakdowns, and the test of LAD22 detection on a
national file pins routing off.

Suites: executor 1832 passed; Pyright stays at 71; ruff and the quality
guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_bundle_manifest_metadata now adds local_area_breakdown: 1 to the
routing bundle manifest of an app whose installed policyengine.py bundle
routes UK area regions to a local-area dataset (uk_area_regions_routed()),
and leaves the key out otherwise. The routing publisher builds the
manifest only for the app it deploys, so frozen older apps, such as
py6-7-5, keep manifests without the key. The next commit makes the
gateway refuse breakdown requests for those apps.

The value comes from the bundle manifest packaged inside the installed
policyengine package; the change reads no new environment variable.

Test, written first:
test_bundle_manifest_declares_local_area_breakdowns_only_for_routed_area_regions.
Before the change its routed case failed with KeyError:
'local_area_breakdown'; the unrouted case passed, as the key was never
written.

Suites: executor 1834 passed; Pyright stays at 71; ruff passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frozen worker apps, such as py6-7-5, accept and ignore unknown payload
fields, so a breakdown=local_areas request routed to one would silently
come back as a national result. submit_simulation now calls
_require_route_serves_breakdown(request, route) inside its existing
route-resolution try block: a breakdown request whose route's bundle
manifest does not declare local_area_breakdown version 1 gets a 400
before anything is spawned. The message names the policyengine.py
version and app the request resolved to. Routes from the legacy version
dicts have no manifest and are refused the same way. Requests without
breakdown are unchanged, and the submission's observability context now
records breakdown.

Tests, written first (new tests/test_local_area_breakdown.py): a worker
without the capability, an older pinned worker (3.9.0) and legacy-only
routes all returned 200 and spawned the job before the change; they now
get 400 with no spawn. The tests that a declaring worker receives the
breakdown and that a national request needs no capability passed before
the change, since the previous commits already forward the field.

Suites: gateway 267 and entry 668 passed; gateway Pyright has no errors;
ruff and the quality guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In projects/policyengine-apis-integ/tests/simulation/test_calculate.py:

- test_calculate_uk_local_area_breakdown (beta_only) submits a UK-wide
  breakdown=local_areas request pinned to the deployed UK model and
  checks the result: kind local_areas, a dataset whose name ends in
  _dense, at least one constituency and one local authority, and a
  disclosure_tier of reported or approximate on every published area.
  This also checks that disclosure control does not withhold every area
  of a level on the real local-area dataset.
- test_calculate_uk_model now also checks that the national result has
  no constituency_impact or local_authority_impact.
- assert_uk_local_area_breakdown and assert_no_uk_area_results accept
  the generated client's objects as well as plain mappings.

Tests, written first: the local tests of the two helpers (a generated
LocalAreaBreakdownOutput parsed from a job status, four incomplete
breakdowns, and national results with and without area data) failed with
NameError before the helpers existed, and pass now. The two beta tests
need a deployment and run in the beta deploy's integration suite.

The local apis-integ tests pass, except test_ping, which needs the
docker-compose services and could not connect here; Pyright stays at the
apis-integ baseline's 4 errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The gateway README gains a "UK local-area breakdowns" section: the
  breakdown field and its rules, the LocalAreaBreakdownOutput fields,
  the local_area_breakdown manifest capability and the gateway's 400,
  the null area fields of UK-wide and nation runs, the reported stages,
  and the Stage 12 skip.
- The Stage 12 migration doc lists the four household columns the area
  outputs read, and says the production national run now leaves the area
  breakdowns out too, by the same uk_area_breakdowns_omitted rule, with
  breakdown requests skipped by Stage 12.
- docs/engineering/skills/microdata-weights.md says which pair, and so
  which dataset's weights, the UK area breakdowns aggregate.
- changelog_entry.yaml replaces #881's entry about the second baseline
  and reform run with one about national runs no longer building it, and
  adds the field and result, the manifest key and gateway 400, the
  earlier column check, and the Stage 12 skip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a bundle that did not route UK area regions, _resolve_region built a
weight-matrix region for a constituency or local authority
(_build_uk_weight_replacement_region), and the runtime and the Stage 12
worker then checked the matrix against the certified release
(_require_certified_uk_weight_matrix). From this branch onward, every
executor app is built with policyengine.py 6.10.0 or later, which routes
those regions to the local-area dataset, and apps already deployed for
older releases are never rebuilt (only the gateway and entry are redeployed from main),
so nothing uses the fallback any more. The two functions, the
runtime's and Stage 12's calls, tests/test_uk_weight_matrix_guard.py and
the _forbid_weight_matrix_regions test helper are deleted.

Test, written first:
test_resolve_region_rejects_uk_area_regions_on_an_unrouted_bundle replaces
the test that kept weight-matrix regions until routing. Before the
change both of its cases failed with DID NOT RAISE <class 'ValueError'>:
the fallback built a WeightReplacementStrategy region. They now get
"Unsupported UK region".

Three executor Pyright errors were in the deleted code, so the baseline
shrinks from 71 to 68 entries and registry.toml's count follows (a
change under rules/, which needs code-owner approval); docs/generated
rules are rendered again. The changelog records the removal.

Suites: executor 1826 passed; Pyright matches the 68-entry baseline;
ruff and the quality guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 6 commits October 23, 2026 00:02
uk_area_regions_routed() was true whenever the bundle manifest's
region_datasets named both area region types. That decides whether a
national file may lack area codes, whether area regions resolve through
.py's registry, whether a breakdown may run, and whether the routing
manifest declares local_area_breakdown. Two bundles .py can certify
were read wrongly:

- regional_release_kind "area_coded_national" routes both area types to
  an area-coded national dataset, but .py's region registry lists no
  constituencies or local authorities for it. With the weight-matrix
  fallback removed, every area request would fail, yet the app would
  still declare local_area_breakdown: 1.
- Both types routed to different datasets. routed_uk_area_dataset_name()
  then fails every breakdown, which again would be declared.

uk_area_regions_routed() now requires regional_release_kind
"local_area" (the condition .py's area_regions_routed_to_local_release
uses) and routed_uk_area_dataset_name() to name one certified dataset.
The routing manifest's capability follows from it, so it is exact too.

Tests, written first, through the real check with a faked manifest:
- test_area_regions_count_as_routed_only_to_one_local_area_dataset: the
  area_coded_national, missing-kind and different-datasets cases failed
  with "assert True is False".
- test_bundle_manifest_declares_local_area_breakdowns_only_for_one_local_area_dataset
  (replacing the test that patched the routing check): the same three
  cases failed with "'local_area_breakdown' not in {...
  'local_area_breakdown': 1 ...}".

Suites: executor 1831 passed, 22 skipped; Pyright stays at
68; ruff passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A constituency or local-authority run on the routed local-area dataset
was checked by detect_uk_local_authority_metadata, which reads only
la_code_oa. A release missing constituency_code_oa, region_code_oa or
source_household_id ran both simulations and the impact analysis
before the disclosure output raised. The runtime and the Stage 12
worker had the same gap.

detect_uk_run_dataset_metadata() in uk_local_authority_metadata.py now
runs the full routed-dataset check (detect_uk_area_dataset_metadata,
all four UK_AREA_DATASET_COLUMNS, naming the dataset and the column)
when the run needs area codes (uk_area_codes_required), the bundle
routes area regions, and the loaded dataset is the routed one. Every
other run keeps detect_uk_local_authority_metadata. Both
_run_simulation_impl_core and the Stage 12 worker call it right after
the dataset loads.

Tests, written first, over the four columns for a constituency and a
local-authority region:
- runtime (test_a_routed_area_run_needs_every_column_its_outputs_read_before_simulating):
  three columns failed with "DID NOT RAISE <class 'ValueError'>" after
  both simulations were built; la_code_oa raised the generic "contains no
  la_code_oa column" error, which does not name the dataset.
- Stage 12 (test_stage12_area_run_needs_every_column_its_outputs_read_before_simulating):
  three columns reached the simulation build ("simulated a dataset it
  cannot use"); la_code_oa raised the same generic error.
A further runtime test pins that a complete area dataset still yields its
LAD23 boundaries. The Stage 12 load helper now fakes the new function.

Suites: executor 1848 passed, 22 skipped; Pyright stays at 68; ruff
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
local_area_breakdown_error() accepted any country/<name>, so a request
such as country/ni (the Public API's spelling) or country/atlantis passed
the contract's 422 check and failed only later, as a job. The contract
now accepts only uk and the four nation regions policyengine.py 6.10.0
registers (UK_NATION_REGIONS: country/england, country/scotland,
country/wales and country/northern_ireland), in any case. Any other
region gets a 422 whose message lists the accepted values.

The gateway installs no country package, so the contract cannot read
.py's registry. An executor test checks UK_NATION_REGIONS against the
nation regions of the installed registry instead.

Tests, written first:
- the request test's country/ni and country/atlantis cases failed with
  "DID NOT RAISE ValidationError";
- test_an_unknown_nation_is_refused_with_the_nations_a_breakdown_takes
  failed with "assert None is not None";
- test_breakdown_nations_are_the_uk_nations_policyengine_registers failed
  with AttributeError: no attribute 'UK_NATION_REGIONS'.
The accepted-region test now covers all four nations and a mixed-case
spelling.

Suites: contract 319, gateway 267, entry 668 and executor 1849 passed;
the gateway OpenAPI golden is unchanged; contract Pyright has no errors;
ruff passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LocalAreaBreakdownOutput.constituency_impact is now
  list[ConstituencyImpactRecord] and local_authority_impact
  list[LocalAuthorityImpactRecord], instead of GeographicImpactOutput,
  whose list takes either record type. A local-authority record can no
  longer validate in the constituency list, or the reverse. The JSON
  shape is unchanged: both fields are still arrays of the same records.
  The executor's builder converts each level's output with
  _area_records(), which refuses a record of the other level.
- UKAreaSimulations.baseline and .reform are typed as the
  StageReportingSimulation that _build_simulation returns, and .dataset
  as Dataset.
- SimulationOutputBuilder._uk_geography_disclosure is typed as
  UKGeographyDisclosureOutput | None.

The gateway OpenAPI golden is regenerated: the two fields now reference
their record schemas as arrays, and the model description gains one
sentence. The generated client is regenerated by make check.

Tests, written first: test_each_area_level_takes_only_its_own_records
failed for both levels with "DID NOT RAISE ValidationError", and
test_each_area_level_reads_its_records_as_its_own_type failed because the
lists were not of the level's record type.

Suites: contract 322, gateway 267, entry 668 and executor 1849 passed;
make check passes with the executor at its 68-error Pyright baseline;
the generated-client tests that need no services pass; the quality
guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SimulationOutputBuilder had two modes: serialize() for the macro
output and serialize_local_area_breakdown() for a breakdown, with fields
that applied to only one of them (resolved_dataset_name to the
breakdown, omit_uk_area_breakdowns to the macro output). Nothing stopped
a caller building a breakdown without a dataset name, or a macro output
with one.

The breakdown now has its own frozen dataclass, LocalAreaBreakdownBuilder
in the new simulation_output_local_areas.py. Its required fields are
exactly what a breakdown needs: the local-area baseline and reform, the
authority boundary metadata, the dataset name and revision, the year
that labels the disclosure release, and the country module for the model
version. _run_local_area_breakdown_core builds it and calls serialize().
SimulationOutputBuilder loses resolved_dataset_name, both breakdown
methods and _policyengine_version.

No behaviour changes: the output, the span names and their nesting, the
stage order (baseline, reform, then outputs) and the data-version
fallback to the release's data version are the same. The year is the
request's parsed year, which is also the year of the dataset loaded for
it.

Tests: the existing breakdown tests are unchanged in what they check.
Only their seams moved: the builder tests construct
LocalAreaBreakdownBuilder, and the runtime fakes replace it and assert
its arguments.

Suites: executor 1849 passed, 22 skipped; Pyright stays at 68; ruff and
the quality guards pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests of behaviour the previous commits already have, so both
passed when first run:

- test_the_installed_bundle_routes_area_regions_to_the_dense_dataset
  reads only the manifest packaged with the installed policyengine.py
  6.10.0, with no network access. It checks that
  routed_uk_area_dataset_name() is microcosm_uk_2024_25_dense, that
  uk_area_regions_routed() is true and agrees with .py's
  area_regions_routed_to_local_release(), and that
  _manifest_dataset_revision() returns that dataset's
  microcosm-uk-2024-25-dense-... revision from the manifest. The other
  routing tests fake the manifest; this one fails if a .py update changes
  the real routing.
- test_a_worker_pinned_by_uk_model_version_without_it_is_refused: a
  breakdown pinned with version (a UK model version) to an older app
  whose manifest declares no local_area_breakdown gets 400 naming
  policyengine.py 3.9.0, with no spawn. The same pinned national request
  still routes to that app.

Suites: gateway 268 and executor 1850 passed; ruff passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 7 commits October 23, 2026 00:22
changelog_entry.yaml:
- Removes the entry saying the release bound UK weight matrices to the
  certified data release; the weight-matrix fallback is removed in this
  release.
- The weighted-microdata entry no longer says one recorded exception
  remains: the list is empty.
- The entry presenting the end of national-run area computation as a
  fix is replaced. That computation was this PR's own unreleased code,
  not shipped behaviour. The new "changed" entry describes the shipped
  behaviour instead: on a bundle that routes UK area regions to a
  local-area release dataset, UK-wide and nation-level runs return null
  area fields and a breakdown run computes them; on other bundles they
  still compute both.
- The weight-matrix removal entry says what a deployment rebuilds: only
  the app for the current .py version, now 6.10.0.
- The breakdown entries name the four accepted nations, the exact
  routing condition, the per-level record types, version-pinned
  refusals, and the column check for constituency and local-authority
  runs.

The gateway README's breakdown section lists the accepted nations and
the exact condition under which the routing publisher declares the
capability.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/engineering/skills/microdata-weights.md named
serialize_local_area_breakdown, which the builder refactor replaced with
LocalAreaBreakdownBuilder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The executor image smoke (smoke_app._validate_installed_uk_local_authority_dataset)
and the Stage 12 static-files smoke
(validate_installed_uk_local_authority_dataset) read only la_code_oa from
the dataset that serves UK area results, which on .py 6.10.0 is the
downloaded microcosm_uk_2024_25_dense file. A release of that file
missing constituency_code_oa, region_code_oa or source_household_id
passed both smokes and failed only when a request ran.

Both smokes now call detect_uk_area_metadata_from_hdf() in
uk_local_authority_metadata.py. It requires every column in
UK_AREA_DATASET_COLUMNS in the household table, then detects the
authority boundary version as before. The column check selects zero
rows, so it reads only the table's column names, not its data.
uk_hdf_household_has_local_authority_codes() shares the same column
reader. The PR image smoke therefore checks the real file's columns
before merge.

Tests, written first, with a real table-format HDF file missing one
column:
- test_uk_dataset_smoke_requires_every_area_output_column
  (tests/test_smoke_app.py);
- test_v2_static_runtime_file_smoke_requires_every_area_output_column
  (tests/test_stage12_modal_app.py).
Before the change, in both files, the constituency_code_oa,
region_code_oa and source_household_id cases failed with "DID NOT RAISE
<class 'ValueError'>". The la_code_oa case raised the generic "contains
no la_code_oa column" error, which does not name the file. A further
test pins that a complete file still gives lad23. The existing smoke
tests fake the detection under its new name.

Suites: executor 1859 passed, 22 skipped; Pyright stays at 68; ruff
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
LocalAreaBreakdownBuilder carried copies of SimulationOutputBuilder's
_span, _model_version and _data_version, including the data-version
fallback. Both builders now use one module,
simulation_output_provenance.py:

- output_span(runtime, name) opens the span with
  scopes.span(runtime, name), as docs/engineering/skills/type-checking.md
  asks, or does nothing when no runtime is given;
- output_model_version(country_module, runtime=...) reads the country
  model's version inside the output_model_version span;
- output_data_version(country, runtime=..., resolved=...,
  simulation_params=..., dataset=...) returns the resolved revision, then
  a data_version request parameter, then the dataset's metadata, then the
  release's data version, or an empty string, inside the
  output_data_version span.

SimulationOutputBuilder keeps its _span, _model_version and
_data_version methods as one-line calls to these, so its many span sites
are unchanged. The breakdown builder calls the helpers directly.

No behaviour changes: the span names and nesting, the version values
and the fallback order are the same, and the existing tests pass
unchanged.

Suites: executor 1859 passed, 22 skipped; Pyright stays at 68; ruff
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LocalAreaBreakdownBuilder.country_module is typed as ModuleType, what
  _country_module returns, instead of Any.
- UKAreaSimulations loses its dataset field. Since the breakdown moved to
  its own builder, nothing reads it: the builder needs the dataset's name,
  which the pair still carries, not the loaded dataset.

No behaviour changes; the existing tests pass unchanged.

Suites: executor 1859 passed, 22 skipped; Pyright stays at 68; ruff
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two tests of behaviour the earlier commits already have, so both passed
when first run:

- test_a_scotland_breakdown_resolves_against_the_installed_bundle runs a
  country/scotland breakdown through _run_simulation_impl_core with the
  real country module, region resolution, dataset selection, routing and
  revision lookup of the installed .py 6.10.0 bundle. Only the dataset
  load, the simulations and the breakdown builder are faked, so no data
  or network is needed. It checks:
  - the one selection loaded is the non-default
    microcosm_uk_2024_25_dense;
  - both simulations use the nation's row filter (region == SCOTLAND);
  - the builder gets that dataset's manifest revision.
- test_an_area_run_on_another_dataset_keeps_the_authority_code_check
  covers detect_uk_run_dataset_metadata's other branch. An area run on a
  dataset other than the routed one still gets only the la_code_oa
  check: it passes without the other three columns, and it fails without
  la_code_oa. A companion test shows the same dataset named as the routed
  one is refused for its missing constituency_code_oa.

Suites: executor 1862 passed, contract 322, gateway 268 and entry 668;
ruff passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The changelog entry for the image smokes now says that they also require
the routed dataset's household table to have all four columns the area
outputs read, checked from the column names only.

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

@PavelMakarchuk PavelMakarchuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks María and Anthony, this looks good to me. I approve.

I merged main (with #904) into it locally, and checked:

  • It merges cleanly. .py 6.10.0 still gives relative_change as a percentage, so #904's /100 stays right. Its test runs on 6.10.0 and passes.
  • All tests pass on the merged tree: executor 1,867, contract 322, gateway 269, entry 668. Ruff 0.15.22 and pyright pass.
  • The 6.10.0 registry has 650 constituencies (18 in NI), 361 local authorities and country/northern_ireland, which matches api#4024.
  • The breakdown run loads the dense file, so it cannot share a baseline ID or a cache entry with a national run. api#4024 also gives breakdown its own options hash.

One question before the UK switch: a region=uk breakdown and a region=country/* breakdown each make their own withholding decisions over the same households. An area that one run withholds could appear in the other, and a user can request both. Is that acceptable under our disclosure policy? If not, a nation request could run through the UK-wide release and then filter. This doesn't need to block the merge.

A small note: breakdown baselines always get a random ID, because the dense file is not the default dataset. So each "Compute" click recomputes the current-law baseline. That is fine for now.

For the record: 6.10.0 pins policyengine-uk 2.132.6, which doesn't have the UC Act uplift yet (policyengine-uk#2242, from 2.135.0). That needs a later .py bump.

Merge order note: #904 is now merged and deployed to prod, so the remaining order is api#4026, then this PR.

@anth-volk

Copy link
Copy Markdown
Contributor

Thanks, Pavel. On your question about withholding decisions: a region=uk breakdown and a region=country/* breakdown each make their own decisions over the same households. We'll deal with that in #907, which covers deciding a disclosure policy for subnational results (including the option of running every breakdown UK-wide and filtering it to the nation) and measuring how far breakdowns can be split by nation and English region. As you said, it doesn't block this PR.

@anth-volk
anth-volk merged commit 3420928 into main Oct 23, 2026
22 checks passed
@anth-volk
anth-volk deleted the uk-728-area-breakdowns-microcosm branch October 23, 2026 16:12
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: build area results on the routed local-area dataset when the executor takes policyengine.py 6.10.0

3 participants