Skip to content

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

Draft
juaristi22 wants to merge 19 commits into
mainfrom
uk-728-area-breakdowns-microcosm
Draft

juaristi22 wants to merge 19 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. Worker apps for older .py versions are never 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.

Contract

  • SimulationRequest.breakdown: Literal["local_areas"] | None = None. Accepted only for UK requests whose region is absent, uk or country/<nation>. Refused with 422 for other countries, constituency, local-authority or bare-name regions, 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 and local_authority_impact. Both lists are GeographicImpactOutput and 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. The gateway refuses breakdown with 400, before spawning, for any route whose manifest lacks it, including 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

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.

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, and the non-default-dataset rule for deterministic baseline ids.

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: 1826 passed, 22 skipped;
    • contract: 312 passed;
    • gateway: 267 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. It refuses constituency/Aldershot, and country/ni is an unsupported region.

  • 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). The new key comes from uk_area_regions_routed(). That function reads 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):

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 not a .py region; .py names it country/northern_ireland. The Public API change maps it.
  • 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>

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.

UK: build area results on the routed local-area dataset when the executor takes policyengine.py 6.10.0

3 participants