Repository navigation
Conversation
Every policy returned by GET /v2/policies/{policy_id}, GET /v2/policies
and POST /v2/policies now carries missing_parameters: the parameter
paths it sets that the catalog of the running PolicyEngine.py version
no longer has, sorted. It is empty for a policy bound to the running
catalog version, and null when that catalog is not published, since
nothing can then be checked. The field is required in the response
model, so it is always present.
V2PolicyService checks the policies after reading them
(with_missing_parameters): one catalog selection and one read of the
running version's parameters by name, only for policies bound to an
older version. The comparison is parameters_missing_from_model, the
check the dual-write copy uses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v3 reads a saved reform's changes through GET /{country}/policy/{id},
so that response now also carries missing_parameters: the parameter
paths the policy sets that the country model the Public API runs no
longer has, sorted; empty when it has them all; null when the stored
policy is not a parameter-path object or the country has no loaded
model. Existing fields are unchanged.
PolicyParameterService compares the paths with the loaded model's leaf
parameters (PolicyEngineCountry.parameter_names, computed once per
process) using parameters_missing_from_model, the comparison the v2
copy uses. It reads nothing from Supabase, so v1 reads stay on Cloud
SQL and a Supabase outage cannot slow them. The v2 catalog of a release
holds the same leaf parameters as that release's model, so a policy
reported here is one the copy refuses as missing_parameters.
The OpenAPI spec, the changelog fragment and the Stage 10 runbook
describe the field on both the v1 and the v2 routes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the bind limit Review found that GET /v2/policies could now fail with 503: the missing parameter check reads every distinct parameter name of a page's policies on older catalog versions in one IN filter, and a page of up to 500 policies with up to 1,000 values each can bind more than the 65,535 values a PostgreSQL statement allows. read_parameters_by_name now reads names in batches of 1,000. The copy, which reads at most 1,000 names, runs one statement as before. The recording session used in unit tests gains all(), which this read calls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… by type with_missing_parameters returned null for an absent catalog and re-raised a failed catalog query by checking whether the exception's cause was a SQLAlchemyError, which depended on how select_catalog chained it, and no test covered the failure branch. select_catalog now raises MetadataCatalogNotPublishedError, a MetadataCatalogUnavailableError, for an absent catalog: the v2 routes return null for that type only, and the copy's catalog_absent and catalog_query_failed reasons come from the type too. Existing handlers of MetadataCatalogUnavailableError are unchanged. Also from review: - the migration contract registry pins result.item.missing_parameters on the v2 policy create and detail routes, and the generated migration contracts are refreshed; - a test covers create_policy filling in the field; - the Stage 10 runbook no longer says every policy reported as missing is refused by the copy: a copy made before a release removed the parameter is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PolicyRead.missing_parameters defaulted to None, which is also what the response documents for an unpublished catalog, so a future read that skipped the check would have told clients the catalog is unpublished. The field moves to CheckedPolicyRead (and pages of them to CheckedPolicyPage), which only with_missing_parameters builds; the v2 policy service, its route protocol and the response models now take those types, so mypy rejects a route result that was never checked. Tests also cover the field in the GET /v2/policies response, and the catalog-query-failure case again chains the database error, as select_catalog raises it, so the test still shows that neither message reaches the copy log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully 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 #4038
Stacked on #4036, which adds the shared check (
parameters_missing_from_model) used here.Summary
Policy reads now say which parameters a policy sets that the current tax-benefit model no longer has, as
missing_parameters: the parameter paths, sorted;[]when the model has them all;nullwhen they cannot be checked. The field is added next to the existing ones; nothing else in the responses changes.Which routes carry it, and why
nullwhenGET /{country}/policy/{policy_id}PolicyEngineCountry.parameter_names).GET /v2/policies/{policy_id}GET /v2/policies,POST /v2/policiesPolicyItem, so the field is always there.Both checks use
parameters_missing_from_model, the comparison the dual-write copy applies (#4036). The v1 route reads the loaded country model rather than the Supabase catalog: v1 reads stay on Cloud SQL, as the Stage 10 runbook requires, a Supabase outage or its five-second timeouts cannot slow a v1 read, and the loaded model is the one a v1 calculation runs. For one release the two sets are the same: .py builds its catalog from the same country package version's leaf parameters (get_descendants()filtered to parameters), and catalog extraction checks the package version against the bundle manifest. A v1 policy reported here is therefore one a first copy under the running release refuses asmissing_parameters. Computing the set takes about 40 ms once per process for the 102,530 US parameters.Other changes:
read_parameters_by_namereads names in batches of 1,000, so a page of policies on older catalog versions cannot bind more values than PostgreSQL allows (review round 1).CheckedPolicyReadandCheckedPolicyPage, which only the missing-parameter check builds, so mypy rejects a route result that skipped it (review round 2).select_catalograisesMetadataCatalogNotPublishedError(aMetadataCatalogUnavailableError) when the running version's catalog is absent. The v2 routes returnnullonly for that case and still return 503 when the catalog cannot be queried; the copy'scatalog_absentandcatalog_query_failedreasons now come from the type, not from the chained exception.result.item.missing_parameterson the v2 policy routes; the generated migration contracts are refreshed.Migration notes
policy(v1GET /{country}/policy/{policy_id}; native v2/v2/policies).Environment variables
None. The change reads no environment variables and touches no workflow, image or infrastructure.
What this PR waits for
#4036 (the shared check and the dual-write fix) merges first. The app change that shows these parameters and blocks running such policies (PolicyEngine/policyengine-app-v2, linked from the tracking issue) has an effect only once this PR is deployed.
Testing
Each commit was written test-first; the failing run came before the code change.
28febfb63(v2 routes): 12 tests failed first ('PolicyRead' object has no attribute 'missing_parameters', HTTP 500 from the routes, and thePolicyItemrequired fields); then 26 passed in the two files.4eaac4a2c(v1 route): the service tests failed at import (policy_parameter_servicedid not exist) and 3 route tests failed (policy_routeshas nopolicy_parameter_service); then they passed. Against the loaded US model (policyengine-us 2.2.1):gov.dol.minimum_wageandgov.irs.credits.ctc.amount.base[0].amountexist, the nodegov.irs.credits.ctc.amountdoes not count as a parameter, andgov.irs.income.bracket.thresholds.1.WIDOW(renamedSURVIVING_SPOUSE) is reported missing.b8cc4c0c2(review round 1: batches): against the unbatched read, the test failed with one statement holding 2,500 names; then 5 passed.761050053(review round 1: not-published catalog type, contract registry, runbook): a catalog query failure was turned intonullinstead of raising (DID NOT RAISE), and the copy tests failed at import; then they passed.e29bd6e4c(review round 2:CheckedPolicyRead): the new tests failed at import (CheckedPolicyReadandCheckedPolicyPagedid not exist); then 64 passed in the v2 policy and copy test files. Round 2 found no bugs.ruff format --check .,ruff check .,uv run --frozen --extra dev mypy,python scripts/run_quality_guards.py, the Phase 10 policy test group, andpython scripts/export_migration_contracts.py.tests/unitandtests/contractsuites (3,400 passed).Inline suppression added:
# noqa: PLC0415on the in-function import ofpolicyengine_api.countryinloaded_model_parameter_names. Importing that module loads every country model, so a module-level import would load them whenever the routes are imported; the module cannot be added tobanned-module-level-importsbecausemetadata_serviceimports it at module level.household_calculation_serviceuses the same suppression for the same reason.🤖 Generated with Claude Code