Repository navigation
Define each country's years once and refuse society-wide requests for other years - #4212
Merged
Merged
Conversation
US 2022 and 2023 are dropped: households and society-wide calculations
share one list of years per country, US 2024 to 2035 and UK 2024 to 2032.
- policyengine_api/economy_years.py defines them once (US newest first,
UK oldest first, the orders both responses already used).
/{country}/metadata's economy_options.time_period and the v2 catalog's
METADATA_TIME_PERIODS read them, replacing the hand-written US list,
uk_economy_years and the catalog's own lists. The installed
policyengine-uk 2.151.0 projects its data to 2032, as uk_economy_years
returned. Without that function nothing imports policyengine_uk, so its
DEP003 baseline entry goes and the deptry baseline count drops to 14.
- A society-wide request for a year outside its country's list (an annual
time_period, also for the DELETE of a stored failure, or a budget
window's start_year) gets 400 with DATASET_YEAR_UNAVAILABLE in errors,
the simulation service's own code, before any stored result is read or
anything is sent to the simulation service. Windows that start in the
list but end after it are unchanged. Household routes are unchanged.
- Catalog publication no longer compares a stored version's
metadata_time_periods. It replaces them with the staged ones in the same
transaction, logs the country, version and both lists, and reports them
under updated_time_periods. Every other column and table still fails on
a difference; regions keep their behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #4212 +/- ##
==========================================
- Coverage 92.12% 92.12% -0.01%
==========================================
Files 296 296
Lines 16652 16696 +44
Branches 2712 2722 +10
==========================================
+ Hits 15341 15381 +40
- Misses 798 800 +2
- Partials 513 515 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The simulation service does not send DATASET_YEAR_UNAVAILABLE, and it refuses no years: a UK year after 2032 fails inside it. Correct the module docstring, the comment on the code and the message function's docstring: the code is the Public API's, the UK years end with 2032 because that is the last year policyengine-uk 2.151.0 projects its dataset to, and the message follows the one the app expects (PolicyEngine/policyengine-app-v2#1452). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
November 6, 2026 13:56
This was referenced Nov 6, 2026
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4211
Stacked on #4210 (remove
household_options). This pull request's base isremove-household-options, so its diff shows only its own changes. Merge #4210 first.Summary
The maintainer decided on 4–5 November 2026 that US 2022 and 2023 are dropped everywhere: household and society-wide calculations share one list of years per country, defined once in the Public API, US 2024–2035 and UK 2024–2032. A society-wide (economy) request for any other year is refused with a 400, even when a result for that year is stored. Household calculations are not refused; only the menus stop offering those years.
One constant per country
policyengine_api/economy_years.pynow holdsUS_ECONOMY_YEARS(2035 down to 2024) andUK_ECONOMY_YEARS(2024 up to 2032), withECONOMY_YEARSmapping each country to its tuple. It replaces:PolicyEngineCountry.build_microsimulation_options(country.py);uk_economy_years, which read the projection end from the installed policyengine-uk;METADATA_TIME_PERIODSindata/v2/catalog/extraction.py, which is nowECONOMY_YEARSitself.uk_economy_yearswas called with the installed policyengine-uk 2.151.0 (PolicyEngine.py 6.12.0). It returned[2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032]:PROJECTION_END_YEARis 2032, and the capital gains index sets no earlier end./metadataand in the catalog; the UK is oldest first in both./metadata:economy_options.time_periodfor the US loses 2022 and 2023. The UK list is unchanged (2024–2032).uk_economy_yearswas the package's only import ofpolicyengine_uk. Thedeclared-dependenciesguard then failed with "baseline entry no longer applies: DEP003 policyengine_uk". The entry is removed fromrules/baselines/deptry.txt, the count ofapi.dependencies.declared-and-usedgoes from 15 to 14 inrules/registry.toml, anddocs/generated/rules.mdis regenerated. These are rule machinery files, so a core developer other than the author needs to approve them (api.process.rule-machinery-code-owned).The 400 for other years
A society-wide request whose year isn't in its country's constant gets a 400 with the code
DATASET_YEAR_UNAVAILABLE. This is the Public API's own code: the simulation service doesn't send it and refuses no years. No existing code had this meaning in a response the Public API produces. The name already appears inworker_spm.WORKER_INPUT_ERROR_CODESand theSPMValidationErrordocstring on master, which expect the simulation service to send it; those references are outside this pull request and are left as they are. For a US request for 2022, the body is:{ "status": "error", "message": "US results are available for 2024 to 2035. 2022 is outside the years the current US model and data cover.", "result": null, "errors": [{"code": "DATASET_YEAR_UNAVAILABLE", "message": "US results are available for 2024 to 2035. 2022 is outside the years the current US model and data cover."}] }app/src/libs/flagship/calculationError.ts) treats a failure as "year not covered" only whenerrorsholds an item with"code": "DATASET_YEAR_UNAVAILABLE", and shows the top-levelmessageas written. The first and last years in the message are read from the country's constant.EconomyService._validate_economy_year(ineconomy_simulation_requests.py) raisesSPMValidationError("DATASET_YEAR_UNAVAILABLE", message). The route's existing_bad_request_responsethen returns_make_error_response(error, 400, result=None, errors=[{"code": ..., "message": ...}]).SPMValidationErroris the existing error type for typed 400s with a code inerrors, not only SPM ones.GET /{country}/economy/{policy_id}/over/{baseline_policy_id}(get_economic_impact). A stored result for the year is not returned.DELETEon the same URL (delete_failed_economic_impact), which validates its query exactly as the GET does.GET .../budget-window(get_budget_window_economic_impact), on the start year, after the existing window-size and end-year checks.window_size=5(2034–2038). Windows may still end as late as 2099 (BUDGET_WINDOW_MAX_END_YEAR).canonical-spm.mdquery rules and the route docstrings describe the 400.Open question: budget windows that end after the list's last year. Today the Public API accepts such a window and submits one batch job for every year to the simulation service, which refuses no years. It either computes the later years or fails on them, for example on a UK year after 2032, the last year policyengine-uk 2.151.0 projects its dataset to. The route answers with whatever that produces:
statusfailed and the per-year progress, when the batch fails. The years inside the list are not returned then either.Should a window be refused when it ends after the list's last year, or limited to it?
Catalog publication replaces a stored version's year list
The catalog stores each version's years (
metadata_time_periods), keyed by country and PolicyEngine.py version.assert_country_matchesraisedCatalogPublicationErrorwhen they differed from the staged ones. So the release after this change would have failed publication for the already-stored 6.12.0 version, whose US list still has 2022 and 2023._comparison_pairsleavesmetadata_time_periodsout of the model-version comparison, for both the pre-publication check and the final validation.update_stored_time_periods(publication_reconciliation.py) reads the stored and staged lists. When they differ, a change of order included, it sets the staged list on the stored version in the publication's transaction.publication._reconcile_stored_versions(formerly_check_stored_versions) calls it only after every stored version has passed the comparison of every other column and table. So any other difference still fails before anything is changed.replaced the time periods of the stored us catalog version 6.12.0: [2035, ..., 2022] became [2035, ..., 2024], with the country, version and both lists inpolicyengine_attributes.PublicationEvidencegainstime_period_updates, printed asupdated_time_periods(country_id,policyengine_version,previous_time_periods,time_periods). The evidence then still describes what is stored./v2/economy-options?policyengine_version=<older>.docs/migration/stage-9-v2-metadata.mdno longer says that publication never rewrites a stored version or that a version never changes once published. It gains a "Time-period options" section, an evidence entry, and the time periods as the second exception next to regions.SelectedCatalogStore,MetadataResponseCacheandEXPLICIT_VERSION_MAX_AGE_SECONDSmade the same claim, and now give the exception. Processes of the previous release keep the years they already read until they stop. HTTP caches can keep a response with the old years for up to five minutes, or a day for a request namingpolicyengine_version.tests/integration/test_v2_catalog_publication.pyis at 1,453 of the 1,500 lines a test file may have. The new database tests are therefore intests/integration/test_v2_catalog_publication_time_periods.py, which.github/workflows/v2-integration-check.ymlnow runs in the same step.Testing
Run on Python 3.12 (
UV_PYTHON=3.12 uv run --frozen --extra dev pytest), each test selected by its node ID. All pass.New:
tests/unit/test_economy_years.py::test_us_years_are_2024_to_2035_newest_first,::test_uk_years_are_2024_to_2032_oldest_first,::test_catalog_extraction_uses_the_same_years,::test_each_extracted_catalog_version_stores_its_countrys_years(us, uk),::test_the_unavailable_year_message_names_the_years_and_the_request(US 2022, UK 2033).tests/unit/test_country.py::test__metadata_offers_the_countrys_economy_years_in_their_order(us, uk).tests/unit/routes/test_economy_unavailable_years.py:::test_an_annual_year_outside_the_countrys_years_gets_400_and_nothing_runs: GET and DELETE, for US 2022, 2023 and 2036 and UK 2023 and 2033. The test checks the whole body, including the top-levelmessageanderrors[0].code, and that the service's collaborators got no call.::test_a_budget_window_starting_outside_the_countrys_years_gets_400: US 2023 with a window of 3, US 2036, and UK 2023.::test_a_stored_result_for_an_unavailable_year_is_not_returned: a finished US impact is stored for 2022. The GET answers 400, the simulation service stand-in gets no call, and the stored record is unchanged.::test_a_stored_result_for_an_offered_year_is_returned: the same setup for 2024 answers 200 from the stored record.tests/unit/services/test_economy_service_budget_window_requests.py::TestEconomyService::TestGetBudgetWindowEconomicImpact::test__given_window_ending_after_the_countrys_years__still_submits_it: 2034–2038 is submitted as before.tests/unit/v2/test_catalog_publication.py::test_stored_versions_are_compared_without_their_time_periods,::test_time_period_update_warning_prints_country_version_and_years,::test_matching_time_periods_log_nothing.tests/integration/test_v2_catalog_publication_time_periods.py, run against a disposable local PostgreSQL 17 container:::test_stored_version_takes_the_staged_time_periods_and_logs_them: a version is stored with US 2035–2022. Republishing stores 2035–2024, keeps every row ID, logs the warning and fillsupdated_time_periods. Republishing again reports nothing.::test_stored_version_with_changed_time_periods_still_fails_on_other_tables: a changed variable label still fails with "differs from the staged catalog in its variables", and the stored years are unchanged.Changed:
tests/unit/test_country.py::TestUKCountryMetadata::test__uk_years_are_years_the_model_projects(wastest__uk_years_run_to_the_models_last_projected_year). It now checks that the UK years areUK_ECONOMY_YEARSand are all years the installed policyengine-uk projects to. It therefore fails if a policyengine-uk update projects to fewer years, instead of requiring the constant to follow the model.tests/unit/routes/test_spm_year_worker_polling.py::test_typed_worker_error_is_terminal_and_replays_after_service_recreation,::test_a_dataset_year_error_returns_400_and_replays_from_storageand::test_an_uncertifiable_stored_result_becomes_terminal_instead_of_repeating. Their annual requests asked for US 2036, which is now refused before the worker is polled. They now ask for 2035, the last US year, which still reaches the worker. The budget windows (2035–2036) are unchanged.tests/unit/v2/test_catalog_publication.py::test_completion_evidence_contains_only_reviewed_non_secret_fields: addsupdated_time_periods.tests/unit/test_alembic_workflows.py::test_reusable_v2_integration_check_uses_disposable_databases_and_coverage: checks that the workflow runs the new integration file.Without the service change, the 400 tests failed (14 cases). Without the publication change, both integration tests failed.
No other test expected US 2022 or 2023 in
economy_optionsor the catalog, or the old publication failure for a changed year list.Other checks:
uv run --frozen --extra dev ruff format --checkandruff checkon the 21 changed Python files: pass (ruff 0.15.12, the versionuv.lockpins).python scripts/run_quality_guards.py: every guard passes.lint-imports: 5 contracts kept.mypy: no issues.python scripts/export_migration_contracts.py: no change.BASE_SHA=remove-household-options bash .github/scripts/check_changelog_fragment.sh: 2 fragments,us-years-from-2024.removed.mdandcatalog-time-periods.changed.md.Environment variables
No environment variables are added, removed or changed.
.github/workflows/v2-integration-check.ymlgains one test file in an existing step. The file reads onlyV2_MIGRATION_DATABASE_URL, and throughalembic-v2.inialsoV2_ALEMBIC_DISPOSABLE_TEST, as the step's other publication tests do. Both are literal values in thecatalog-runtime-integrationjob'senv:block, so they reach every step of the job. Callers run the workflow throughuses:and pass no secrets, and it needs none.No inline suppressions are added.
🤖 Generated with Claude Code