Conversation
The wrapper's 6.0.0 release binds policyengine-core 3.32.5, policyengine-us 2.2.1, policyengine-uk 2.90.2 and spm-calculator 1.0.0, and its manifest certifies the SPM measurement with scenario ce_trend, county geography, county vintage 2020 and forecast content sha256 3d86d5c4. Both pip build paths move together: Cloud Run and the published GHCR image install with pip and do not read uv.lock, so the separate spm-calculator pin has to move with the wrapper or the image resolves a calculator the country model cannot use. The lock was refreshed from PyPI only; the resolved wheels are ac51a637 (policyengine), 0993a6c7 (policyengine-us) and e354937a (spm-calculator, not the later 1.0.0.post1 that country 2.2.1's range would also admit), and the lock's own project version catches up to pyproject's 3.56.1. The automatic bundle updater cannot make this change. It rewrites only the policyengine[models] pin and then runs uv lock, and 6.0.0's models extra pins spm-calculator 1.0.0 against this project's standalone 0.3.1, which is unsatisfiable. Its dispatched run for 6.0.0 failed on exactly that resolution and opened no PR. The housing-cap tests exercised the SPM geography requirement with households that receive no housing assistance, where the cap is not the reason the measurement is needed. They now request a controlled positive award where the requirement is the point, and assert the unassisted case succeeds with a zero housing resource, no measurement-year receipts and no geography. Ordinary household_benefits and household_net_income count the actual award rather than the capped SPM resource, so they need no SPM geography at all and do not move with a valid selection; the contract document says so. These tests skip on a legacy install, so they were run against an installed canonical bundle rather than relied on in CI. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
tests/integration/test_v2_catalog_installed.py bounds the catalog the v2 seed job publishes by asserting exact entity counts, and those counts are a property of the installed country models. policyengine-us 1.764.6 to 2.2.1 moves four of them: variables 6,649 to 7,046, parameter nodes 27,813 to 29,118, parameters 99,006 to 103,705 and parameter values 1,172,130 to 1,192,826. Models, model versions, datasets and regions are unchanged, as are the dataset names and the US fallback summaries the same test asserts. The new numbers were produced twice from the canonical bundle and agree exactly: by this repository's own Cross-database integration job on the previous commit, and locally against an installed policyengine 6.0.0 environment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests.
Additional details and impacted files@@ Coverage Diff @@
## max/spm-api-cache-capability-20260912 #3837 +/- ##
==========================================================================
- Coverage 88.08% 48.30% -39.78%
==========================================================================
Files 180 165 -15
Lines 10957 9832 -1125
Branches 1934 1683 -251
==========================================================================
- Hits 9651 4749 -4902
- Misses 778 4687 +3909
+ Partials 528 396 -132 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The canonical PolicyEngine 6.0.0 bundle certifies an SPM measurement, so `normalize_spm_selection` resolves a selection for every US request where a legacy bundle returned None. Three boundaries that were unreachable before then activate on every request: the API requires the selected worker to advertise a matching capability before it submits, it hands the measurement to the country package, and it refuses a worker result or a stored household calculation that carries no receipt for what was measured. Ninety-five unit tests were written while those boundaries were dormant, and their doubles answer none of them: plain MagicMock gateways return an attribute instead of a capability, mocked `/versions` surfaces serve no registry, country doubles accept only the two-argument call, and worker results carry no provenance. On the previous commit the CI-order suite was 95 failed, 2186 passed under the 6.0.0 pins; it is now 2286 passed, 0 failed. The doubles now answer the real contract, held in one place in `tests/fixtures/spm.py`: the worker capability, the gateway `/versions` registry document, the country provenance receipt, the measurement arguments a country package receives, and the request options the service resolves. Every value is read from the installed bundle manifest through `normalize_spm_selection` rather than transcribed, so a double follows the pinned bundle instead of becoming a second copy of it that the next bump would contradict, and none of these files now names an artifact hash. On a bundle whose model predates the contract every helper returns the legacy shape; the economy-service cache-identity constants are then byte-identical to the ones this commit replaces. Tests that assert legacy behaviour by name pin an uncertified bundle instead of inheriting one from whatever is installed. That is `test_a_model_this_build_cannot_load_reads_as_no_canonical_model`, whose docstring already says "uncertified bundle"; `test_actual_settings_validator_rejects_explicit_settings_on_legacy_worker`, where a legacy manifest describes a legacy install only together with the model it names; `test_legacy_country_requests_do_not_receive_spm`; and `SimulationAPIModal`'s `TestRun` and `TestRunBudgetWindowBatch`, which assert the legacy gateway body — an explicit data artifact revision dropped, no measurement attached. The canonical translation of the same payloads is already covered against real HTTP in `tests/unit/services/test_worker_spm.py`. `test_spm_settings.py` gains the certified counterpart of the first of those: whatever stops the country import on a certified bundle must reach the caller as a typed configuration failure rather than a 500. Three expectations changed rather than doubles, because the canonical contract makes the old ones wrong. `test__given_request_context__then_all_calls_forward_request_id` asserted the exact set of gateway paths a sequence of calls touches. A certified bundle reads the worker registry before each submission, so `/versions` joins that set; the test's subject is that every call carries the request id, and the capability probe is one of them. `test__given_no_previous_impact__creates_new_simulation` asserted that the options written with the start claim are the options the caller sent. The service resolves the measurement into the options before anything is hashed, claimed or submitted, so the stored row records what was submitted; it now asserts the resolved options. The same resolution is why the cache-identity constants gain an `spm` segment. `test_household_under_policy_calculates_and_caches_json_as_an_object` asserted that the country is called with a household and a policy alone. A certified bundle also passes the resolved measurement and whether the saved household requested one, so the expected call carries both. Two doubles were malformed rather than incomplete under the canonical contract and are now well-formed: budget-window results that declared a three-year window while carrying zero or one annual impacts, which the API reads as a window with incomplete receipts; and `test__given_bundle_default_dataset_name__canonicalizes_setup_identity`, which built setup options against the module-level live HTTP client and so attempted a real network call once the capability probe became reachable. Verified locally against an installed canonical environment (policyengine 6.0.0, core 3.32.5, us 2.2.1, uk 2.90.2, spm-calculator 1.0.0; CPython 3.14.4 arm64). No assertion was weakened or removed, and nothing is skipped or xfailed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Summary
Moves the API's PolicyEngine bundle from 5.2.0 to the canonical 6.0.0 tuple and raises the separate
spm-calculatorpin from 0.3.1 to 1.0.0.policyengine6.0.0 was published to PyPI at 2026-09-15T13:37:27Z. Itsmodelsextra bindspolicyengine-core==3.32.5,policyengine-us==2.2.1,policyengine-uk==2.90.2andspm-calculator==1.0.0, and its bundle manifest certifies the SPM measurement (scenarioce_trend,geography_kindcounty,county_vintage2020,forecast_content_sha2563d86d5c4c0423480e6b69b75d222ffa4a7a2639e4094df5ba2504af01be17173). The release's constraints file is attached to https://github.com/PolicyEngine/policyengine.py/releases/tag/6.0.0.Stacked on #3834
This branch is cut from #3834's head
466740d589017df0eb76eb09c59672b806047058, not frommaster, and targets that branch. #3834 is still open and unmerged, and the bump depends on it:check-policyengine-bundle-supported.shonmasterhas neitherextract_calculator_versionnor--check-installed-spm, so onmastera registered 6.0.0 worker with emptyspm_capabilitieswould pass the release gate and every canonical economy request would then fail withSPM_CONFIGURATION_UNAVAILABLE. Nothing on #3834's branch was touched here. When #3834 merges, retarget this PR tomaster.Why the automatic updater did not do this
The wrapper's consumer-update dispatch reached this repository at 2026-09-15T13:43:43Z and the resulting run failed: https://github.com/PolicyEngine/policyengine-api/actions/runs/34976880739.
update-policyengine-package.shrewrites only thepolicyengine[models]pin and then runsuv lock --upgrade-package policyengine, which cannot resolve — 6.0.0'smodelsextra itself pinsspm-calculator==1.0.0while this project separately pins==0.3.1:No
auto/update-policyengine-bundle-6.0.0PR exists, on this repository or onpolicyengine-sim-api(whose dispatch, run 34976880733, failed the same way). This PR is the change the updater could not make; there is nothing to close. The earlier stale auto PRs (#3828, #3809, #3805, #3804) are untouched and would hit the same resolution failure if retriggered.Changes
pyproject.tomlpolicyengine[models]==5.2.0→==6.0.0;spm-calculator==0.3.1→==1.0.0docker/Dockerfilepip installlineuv.locktests/unit/test_cloud_run_deploy_scripts.py..._for_the_legacy_country_bundleto..._for_the_canonical_country_bundletests/unit/test_bundle_update_pins.pytests/unit/test_country_spm.pytests/integration/test_v2_catalog_installed.pydocs/canonical-spm.mdchangelog.d/canonical-bundle-6-0-0.changed.mdtests/fixtures/spm.py(new) + 13 test filesBoth pip paths move together because Cloud Run and the published GHCR image install with pip and do not read
uv.lock; leaving the standalone calculator pin behind would let those images resolve a calculator the country model cannot use.Resolved wheel digests in the refreshed lock, each checked against the release manifest:
policyengineac51a637881744703939d661c94d843aa773e363bf2ba1e418b26592fa210aecpolicyengine-us0993a6c73fcdfbe171a796aca9d302741bd8e00ab83388ad81c15a08740318c6spm-calculatore354937a5e1a4045d4966ed594a528d8b02866fabaac9bb5672017004b627305spm-calculatorresolved to 1.0.0 and not the later1.0.0.post1, which country 2.2.1's>=1.0.0,<=1.0.0.post1range would also admit; the installeddist-infoisspm_calculator-1.0.0.dist-info.policyengine-coremoves 3.30.1 → 3.32.5 andpolicyengine-ukwas already at the manifest's 2.90.2. The lock's ownpolicyengine-apiversion also catches up from 3.54.2 to pyproject's 3.56.1, which had drifted on the base branch.Housing-cap fixtures
The SPM housing-cap tests exercised the geography/year requirement with households receiving no housing assistance, where the cap is not the reason the measurement is needed. They now request a controlled positive award where the requirement is the point, and assert that the unassisted case succeeds — 200, zero housing resource, empty
spm_provenanceyears and geographies.docs/canonical-spm.mdrecords the corresponding contract: ordinaryhousehold_benefitsandhousehold_net_incomecount the actualhousing_assistancerather than the capped SPM resource, so they need no SPM geography and do not move with a valid selection.These tests skip on a legacy install (
pytest.skip("Requires the coordinated canonical US model and calculator source")), so they are silent in CI until the canonical bundle is installed. They were run against an installed canonical bundle instead:87 passedfor the file,26 passed, 0 skippedfor the housing subset, on policyengine 6.0.0 / core 3.32.5 / us 2.2.1 / uk 2.90.2 / spm-calculator 1.0.0, CPython 3.14.4 arm64.Installed-catalog entity counts
tests/integration/test_v2_catalog_installed.pybounds the catalog the v2 seed jobpublishes by asserting exact entity counts, and those counts are a property of the
installed country models.
policyengine-us1.764.6 → 2.2.1 moves four of them:variablesparameter_nodesparametersparameter_valuesmodels,model_versions,datasetsandregionsare unchanged, as are the datasetnames and the US fallback summaries the same test asserts. The new numbers were
produced twice and agree exactly: by this repository's own
Cross-database integration testsjob on commitd7cc47e3, and locally against an installed canonical bundle. Thetest now passes locally (
1 passed), and theCross-database integration testscheckpasses in CI on this head after failing on the previous one.
Status: draft, and one thing blocks it
1. The bundle-supported check cannot pass yet.
pr.ymlrunscheck-policyengine-bundle-supported.sh --if-changed-from-base, which skips only when the wrapper pin, the calculator pin and the SPM integration files are all unchanged from the base. Both pins move here, so the check runs and asks the simulation gateway whether the 6.0.0 bundle is deployed. It is not. The check ran and failed with, verbatim:The deployed simulation API tops out at 5.2.0, and no canonical PR is open on
policyengine-sim-api. This check is expected to fail until the simulation API's 6.0.0 worker is deployed.Docker,Cloud Run container,Test environment variablesandTestall declareneeds:on it, and all four did skip.2.
The canonical bundle turns 95 existing unit tests red.Fixed in7d357a2— see below.Canonical-bundle test doubles
A certified bundle makes
normalize_spm_selectionresolve a measurement for every US request where the legacy bundle returnedNone, which activates three boundaries that were previously unreachable: the API requires the selected worker to advertise a matching capability before it submits, it hands the measurement to the country package, and it refuses a worker result or a stored household calculation that carries no receipt for what was measured. Ninety-five unit tests were written while those boundaries were dormant and their doubles answer none of them — plainMagicMockgateways return an attribute rather than a capability, mocked/versionssurfaces serve no registry, country doubles accept only the two-argumentcalculate, and worker results carry no provenance. Oneb4f46e3the CI-order suite was 95 failed, 2186 passed; it is now 2286 passed, 0 failed.The contract now lives once in
tests/fixtures/spm.py: the worker capability, the gateway/versionsregistry document, the country provenance receipt, the measurement arguments a country package receives, and the request options the service resolves. Every value is read from the installed bundle manifest throughnormalize_spm_selectionrather than transcribed, so the doubles follow the pinned bundle instead of becoming a second copy of it that the next bump would contradict — no test file added here names an artifact hash. On a bundle whose model predates the contract each helper returns the legacy shape, and the economy-service cache-identity constants are then byte-identical to the ones this commit replaces (checked by re-importing the fixture module with the selection forced toNoneand diffing againsteb4f46e3).Tests that assert legacy behaviour by name pin an uncertified bundle rather than inheriting one from whatever is installed:
test_a_model_this_build_cannot_load_reads_as_no_canonical_model(whose docstring already says "uncertified bundle"),test_actual_settings_validator_rejects_explicit_settings_on_legacy_worker(a legacy manifest describes a legacy install only together with the model it names),test_legacy_country_requests_do_not_receive_spm, andSimulationAPIModal'sTestRun/TestRunBudgetWindowBatch, which assert the legacy gateway body — an explicit data artifact revision dropped, no measurement attached. The canonical translation of the same payloads is already covered against real HTTP bytests/unit/services/test_worker_spm.py::test_actual_entrypoint_http_preserves_selection_and_data_versionand…::test_canonical_direct_submission_forwards_the_validated_default_bundle, so nothing is left uncovered.tests/unit/test_spm_settings.pygains the certified counterpart of the first of those (5 new cases): whatever stops the country import on a certified bundle must reach the caller as a typedSPM_CONFIGURATION_UNAVAILABLErather than a 500.No assertion was weakened or deleted, and nothing is skipped or xfailed. Three expectations changed, because the canonical contract makes the old ones wrong:
test__given_request_context__then_all_calls_forward_request_id/versions: a certified bundle reads the worker registry before each submission, and the test's subject is that every call carries the request idtest__given_no_previous_impact__creates_new_simulationspmsegmenttest_household_under_policy_calculates_and_caches_json_as_an_objectspmandspm_requested, which a certified bundle always passesTwo doubles were malformed rather than incomplete under the canonical contract and are now well-formed. Budget-window results declared a three-year window while carrying zero or one annual impacts, which the API reads as a window with incomplete receipts. And
test__given_bundle_default_dataset_name__canonicalizes_setup_identitybuilt setup options against the module-level live HTTP client, so it attempted a real network call once the capability probe became reachable; it now injects a gateway double.By file, the 95 that were failing:
tests/unit/services/test_economy_service.py50,tests/unit/libs/test_simulation_entrypoint.py13,tests/unit/routes/test_economy_submission_identity.py7,tests/unit/test_spm_settings.py5,tests/unit/routes/test_calculate_deprecated_inputs.py5,tests/unit/services/test_household_calculation_service.py4,tests/unit/routes/test_calculate_error_statuses.py3,tests/unit/services/test_worker_spm.py2,tests/unit/routes/test_household_and_user_policy_orm_routes.py2,tests/contract/test_simulation_gateway_contract.py2,tests/unit/routes/test_canonical_spm.py1,tests/integration/test_budget_window_in_flight_dedupe.py1.This remains local evidence only: the
Testjob still declaresneeds: ensure-policyengine-bundle-supported-by-simulation-api, so the unit suite does not run on this PR until a 6.0.0 worker exists. That the bundle was the cause is settled by CI rather than inferred: #3834'sTestjob on head466740d— the exact code this branch is cut from, with the 5.2.0 pins — passed in 7m18s, as did itsCross-database integration tests.Database preflight (G4) — not run, credentials unavailable
This bump must run the ordinary v2 seed job.
catalog_selection.py:70-119selects the catalog row whereTaxBenefitModelVersion.versionequals the running wrapper version —importlib.metadata.version("policyengine"), so6.0.0after this change — and raisesMetadataCatalogUnavailableErrorwhen it is absent, so v2 metadata routes fail until a 6.0.0 catalog is published. The no-seed branch is not available here.The read-only preflight below could not be run from this session: the v2 catalog lives in Supabase (staging
kvvwlyzqrfpvrxinmlwt, productionkvrifaviwhzjztcbrfpy) behind theV2_MIGRATION_DATABASE_URL/V2_DATA_WRITE_DATABASE_URLenvironment secrets, which are not readable. Someone holding those, or Supabase project access to those refs, should run against both staging and production, read-only:Expected: (1) exactly
724b1b11a33e, so the seed job'salembic upgrade headis a no-op and the only writes are the additive catalog; (2)5.2.0present for both countries and no6.0.0row; (3) counts recorded, to be compared after the seed — publication runs in one transaction under a transaction-scoped advisory lock, publishes only absent country versions, asserts existing ones match, and aborts if any of those four counts changed. Because selection is keyed on the running version, the serving 5.2.0 revision keeps reading its own row throughout.Verification
Local, in an installed canonical environment (policyengine 6.0.0, core 3.32.5, us 2.2.1, uk 2.90.2, spm-calculator 1.0.0; CPython 3.14.4 arm64;
uv pip checkclean):pytest tests/unit/test_country_spm.pypytest -k "assisted or zero_award or unknown_county or requires_geography"pytest tests/unit/test_bundle_update_pins.py tests/unit/test_cloud_run_deploy_scripts.pyRUN_V2_CATALOG_COMPATIBILITY=1 pytest tests/integration/test_v2_catalog_installed.py-qeb4f46e3make test's exact command (MAX_HOUSEHOLDS=1000 coverage run -a --branch -m pytest tests/to_refactor tests/unit tests/contract tests/integration/test_budget_window_in_flight_dedupe.py --disable-pytest-warnings)coverage xml -iexit 0uv run --frozen --extra dev mypy(make typecheck-v2)Success: no issues found in 83 source filesruff format --check .445 files already formatted(also clean under ruff 0.16.7, the version CI installs)ruff check .All checks passed!python scripts/run_quality_guards.pymigration-contracts: passedpython scripts/export_migration_contracts.pydocs/generatedunchanged2286 is 2281 + 5: the five new certified-bundle cases in
tests/unit/test_spm_settings.py.CI on head
7d357a29(run 34998954393), all settled:codecov/patchcodecov/projecteb4f46e3. Only theCross-database integration testsjob uploads coverage whileTestis gated off, so project coverage is reported from a fraction of the suite. It clears with the same gate as blocker 1.needs:the failing gate)Not verified here: any live staging or production state, the deployed worker's
spm_capabilities, and the database preflight above.🤖 Generated with Claude Code