Skip to content

Calculate stored households through /v2/household-calculations on the v2 household functions - #4092

Draft
anth-volk wants to merge 39 commits into
v2plan/1f-116-catalog-routes-on-demandfrom
v2plan/2b-204-household-calculations
Draft

anth-volk wants to merge 39 commits into
v2plan/1f-116-catalog-routes-on-demandfrom
v2plan/2b-204-household-calculations

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4087

Summary

The Public API now sends stored households to the Simulation Entrypoint's v2 household functions, records which catalog versions offer SPM settings, and checks both against its own /calculate in staging. Nothing calls the new routes yet: no frontend traffic reaches them.

  • POST /v2/household-calculations takes country_id, household_id, an optional policy_id, optional axes and add_missing_variables. It needs no account.
    • It checks that the country's countries row allows household calculations (422 naming the country otherwise; a country with household calculations only is accepted), that the household and policy exist in that country, through the same services as /v2/households and /v2/policies (404 otherwise), and that the household has a default year (422 otherwise).
    • It selects the country's running catalog version and sends the Simulation Entrypoint only IDs: the country, the household's and policy's IDs, the options (axes as given) and that version's PolicyEngine.py version. The Simulation Entrypoint reads the household, the policy's parameter values and the catalog version from these tables itself, through its own SELECT grant, converts the household, and calculates the household's default_year, which it sends the household function as the request's required year: the year of added variables, and of values and axes without a period.
    • The answer is 202 with the calculation's ID and status. Callers cannot force a new calculation: an identical one that succeeded in the last 28 days is returned again.
  • GET /v2/household-calculations/{calculation_id} returns the calculation as it stands: once it succeeded, the calculated household with its warnings, the PolicyEngine.py version and, for US households, spm_config and spm_provenance; once it failed, its error and the household function's error detail.
  • Simulation service answers. The submission and each answer are checked against the generated simulation client's models, and kept in their own key order, because the household function lists warnings in the household's key order. Axes are checked against the service's limits (equal point counts within a group, at most 10,000 points) before anything is sent.
    • 422, passed through with the simulation service's reason: the caller's data it refuses, which it checks before writing anything: a policy changing a parameter the catalog version lacks, a stored policy value it cannot take (such as an object), or a stored household it cannot take (such as a group no person belongs to, which /v2/households accepts). A request field it refuses gets 422 with each field.
    • 503: its other refusals (a household, policy or catalog version it cannot find, or a household without a default year, which the Public API checks first, so such an answer means the two services disagree; or no v2 worker for the version), an unreachable service, an answer of another shape (including a 422 whose body is not text or a list of fields), a gateway token that cannot be fetched, or missing configuration.
  • SPM availability in the catalog. Catalog publication records, for each US catalog version, whether the Simulation Entrypoint's public GET /versions lists a certified SPM capability for its PolicyEngine.py version, with the settings schema and certified defaults; /v2/model-catalog serves it as spm. A version the service does not serve yet, or an unreadable answer, leaves it unrecorded for the next publication run. /versions itself is unchanged.
  • Route group and logs. /v2/household-calculations belongs to the household route group. Its migration request log records the Supabase read and the v2 workers on the Cloud Run Simulation Entrypoint, not the household group's v1 database and compute flags. The contract registry and the generated migration contract documents include both routes.
  • Staging checks. The parallel live staging job runs, against each tagged staging candidate: a submit, poll and result cycle; six calculations submitted at once, each accepted within 10 seconds and finished within 300 seconds; a household across 401 axis points, which must finish; and a parity set of three US and three UK households (with axes, reforms stored through /v2/policies, added variables, and a deprecated input that produces a warning), compared value by value with the Public API's /calculate and /calculate-full. The checks cannot see how many Simulation Entrypoint instances served them or how much memory the household functions used. The job's time limit rises from 20 to 30 minutes.

Route groups covered: household (new routes only). Everything else stays on its current path: /calculate, /calculate-full and the stored-household calculation routes still calculate in process. User-visible API changes: the two new routes, and spm in /v2/model-catalog.

The parity household that adds missing variables names its person you, because /calculate-full adds them for the first age period of a person named you (or else the current year) while the v2 path adds them for the household's default year; a test with the date fixed in 2027 checks both use the same year.

Testing

Run locally on the branch head (with the simulation client generated from PolicyEngine/policyengine-sim-api#957 at 3a2bbe02 installed over the locked release):

  • uv run pytest tests/unit tests/contract: 3717 passed;
  • against a disposable PostgreSQL 17 container: tests/integration/test_alembic_v2_lifecycle.py and tests/integration/test_v2_catalog_spm_availability.py, 14 passed;
  • ruff 0.15.12 (format --check and check), mypy, and scripts/run_quality_guards.py pass; scripts/export_migration_contracts.py regenerated the contract documents;
  • the two new live modules ran against a local server standing in for a staging candidate (stored households and policies, calculations, /calculate and /calculate-full, a 503 on each first submission): 9 passed; a lasting 500 fails at once and a lasting 503 fails after the outage limit. They have not run against staging: they run only in the staging deployment job.

Environment variables and operator steps

Operator step: a SELECT grant. The Simulation Entrypoint's runtime role simulation_runtime needs SELECT on tax_benefit_models, tax_benefit_model_versions, regions, datasets, parameters, parameter_values, policies and households, granted by the owner of these tables. Until then, calculations answer 503. It stays an operator step for now: once that role exists, a later change declares the grant in the v2 privileges manifest (policyengine_api/data/v2/privileges.py), so each deploy grants and checks it. This PR does not add it there, because the manifest's deploy-time reconciliation would fail in an environment where the role does not exist yet.

  • SIMULATION_ENTRYPOINT_URL
    • Provider: GitHub environment secret, set in staging and production.
    • Runtime: already in the job-level env: of both Cloud Run deploy jobs, and written to the revision by deploy_cloud_run_candidate.sh when it is set. The new routes always call this URL, whatever SIM_ENTRYPOINT selects (both environments select cloud_run_simulation_entrypoint). Without it, both routes answer 503.
    • Catalog publication (new): passed in the publish step's env: in seed-v2-database.yml; that job declares environment: ${{ inputs.deployment_environment }} and is called with secrets: inherit. Publication stops before extraction when it is unset.
  • GATEWAY_AUTH_ISSUER, GATEWAY_AUTH_AUDIENCE, GATEWAY_AUTH_CLIENT_ID, GATEWAY_AUTH_CLIENT_SECRET_RESOURCE: organization secrets, unchanged. The deploy step passes them to the deploy script, which writes them with GATEWAY_AUTH_REQUIRED=1; the client secret is read from Secret Manager at runtime, as for every existing simulation call.
  • Live checks: API_BASE_URL and STAGING_API_TEST_PROBE_ID come from the deploy job's outputs in the live job's step env:, as before. STAGING_HOUSEHOLD_PARALLEL_COUNT, STAGING_HOUSEHOLD_SUBMIT_LIMIT_SECONDS, STAGING_HOUSEHOLD_FINISH_LIMIT_SECONDS, STAGING_HOUSEHOLD_CALCULATION_TIMEOUT_SECONDS and STAGING_HOUSEHOLD_TRANSIENT_LIMIT_SECONDS are optional limits with defaults; none is set.

Not verified: that the SELECT grant exists in staging or production; that each environment's SIMULATION_ENTRYPOINT_URL names that environment's Simulation Entrypoint (the existing release check covers this); and that the gateway's Auth0 client holds the permissions the Simulation Entrypoint will require for its household routes once it enforces them.

Waits for

Suppressions added

  • # noqa: PLC0415 twice in fastapi_routes/dependencies.py: the household calculation service and its simulation client are imported inside the default factory, so the generated client is loaded only on the first calculation request, as the other default factories do.
  • # type: ignore[import-untyped] for policyengine_observability in the simulation client module: the package has no type information, and the module is in mypy's file list.

CI: the Pull request workflow on 4c515e2 failed three jobs, as on every run since the branch was opened.

  • Type-check API v2 and Test: the locked policyengine-api-simulation-client 0.20261027.288 lacks the household calculation models, so mypy cannot find them and tests/unit/v2/test_household_calculation_simulation_client.py fails at import. This is the client release this PR waits for.
  • Cross-database integration tests: test_v2_catalog_installed.py::test_installed_policyengine_catalog_is_complete_and_bounded and the eight test_v2_catalog_metadata_parity.py tests fail with "the installed PolicyEngine.py does not publish catalog format 2's fields". Those tests come from Store the metadata v3 reads in the v2 catalog from PolicyEngine.py's public records (catalog format 2) #4023 in the base; the base PRs' runs skipped this job, and this PR changes neither the catalog extraction nor the installed PolicyEngine.py.

Every other job passed, including lint, the quality guards, both Alembic qualifications and the container builds.

🤖 Generated with Claude Code

anth-volk and others added 30 commits October 22, 2026 16:59
The Public API listed its countries in several places: the v2 routes'
`CountryId` type (`Literal["us", "uk"]`), an unused `SUPPORTED_COUNTRY_IDS`
set in the query models, catalog extraction's `SUPPORTED_COUNTRIES` and
its required packages and installed models, the set of countries the v1
routes copy into v2, and the household copy command's options.

`policyengine_api/supported_countries.py` now holds the one list,
`SupportedCountryId`, and `SUPPORTED_COUNTRY_IDS` derived from it; each of
those places derives from it. The list lives in its own module, not in the
query models, because the query models import the catalog package, which
must import the list too.

Tests: in a fresh interpreter, adding "zz" to the one list adds it to every
derived list (this failed before the change: `CountryId` rejected "zz");
the catalog's per-country settings must cover exactly the listed countries.

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

Adds the v2 `countries` table: one row per country the Public API serves,
with `household_enabled` and `society_wide_enabled` saying which
calculations it supports (a check requires society-wide countries to be
household capable). `users.primary_country` and the `country_id` of
households, policies and the four user association tables reference it,
with `ON DELETE RESTRICT`, instead of each checking `IN ('us', 'uk')`. The
tables that pair v1 records with their v2 copies keep their checks.

Revision `fb1a3e40d3d6` was autogenerated. Its one hand-written operation
inserts the US and UK rows, both capable of both, between the table and the
foreign keys, which otherwise fail on every database whose rows already
name either country. This insert is an exception to the rule that revisions
contain only generated operations, approved for this revision alone; the
revision's comment and the Alembic skill say so. The generated downgrade
restores the checks and drops the table with its rows.

The privilege manifest gives the countries table no role access yet.

Tests (failed first because the `countries` table and `Country` model did
not exist): model structure and foreign keys; SQLite persistence (an
unlisted country is refused, a listed country in use cannot be deleted, a
household-only row lets records name that country); the revision's
operations; and the Postgres lifecycle (upgrade keeps existing US and UK
rows, unlisted countries are refused, downgrade and re-upgrade, a downgrade
is refused while records name another country, and the previous release's
table usage still works after the upgrade).

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

The new script requests /health from the Simulation Entrypoint URL that
SIM_ENTRYPOINT selects, reads the Cloud Run revision from the
x-policyengine-simulation-revision header, and removes the revision's
number and suffix to get the service name. Staging must be served by
policyengine-simulation-entry-staging and production by
policyengine-simulation-entry; a missing header, a name that is not a
Cloud Run revision, a failed request or an unknown environment fails.
The shared old gateway reports no environment and is not checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The staging and production model-version alignment jobs now set
DEPLOYMENT_ENVIRONMENT and run check_simulation_entrypoint_environment.sh
right after checkout, before the bundle compatibility check. Every staging
and production deploy job needs these jobs, so a release whose staging
SIMULATION_ENTRYPOINT_URL is served by the production Simulation
Entrypoint, or the reverse, stops before it deploys.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`GET /v2/countries` lists every row of the countries table, ordered by ID:
each country's `id`, `household_enabled` and `society_wide_enabled`. It
takes no query parameters (any is refused with a 422), needs no user
credential, and answers 503 when the v2 database is not configured or
cannot be reached, and a secret-safe 500 for anything else, through the
shared v2 error mapping. Requests are logged under the `metadata` route
group.

The route follows the v2 layout: a `countries` route package, and a
service package with a read connector, a session container, a
transformation and a read type. The Public API's runtime role gets SELECT
on `countries` in the privilege manifest, which the deploy grants after the
schema upgrade. The migration contract registry gains the
`country_selection_v2` workflow, and the cross-database integration
workflow runs the new integration test.

Tests (failed first: the route and service tests could not import
`policyengine_api.services.v2.countries`, the OpenAPI test did not find
`/v2/countries`, the generated contract files were stale, and the
integration workflow did not name the new test): route responses, the
query refusal, 405 for other methods, 503 and 500 mapping and the OpenAPI
document; the service against SQLite; and, against migrated disposable
Postgres as the runtime role, the route returning the two rows the
revision inserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`policyengine_api.asgi` now checks, while the application loads, that the
v2 `countries` table holds one row for each country in
`policyengine_api/supported_countries.py` and no other. A worker that finds
a difference raises `CountryListMismatchError`, naming the listed countries
without a row and the rows not listed, so it never serves a request; on
Cloud Run the revision then fails its health checks.

The check runs on Cloud Run (`K_SERVICE` is set) and wherever a v2 runtime
database URL or its secret resource is configured. A local or test startup
without v2 configuration opens no connection. A missing table, a missing
privilege and unusable v2 configuration also stop startup. A database that
cannot be reached is logged, by error type only, and startup continues: the
v1 routes do not read the v2 database, and their copies into it already
continue without it.

The countries service gains `verify_countries_match_code_list`, which reads
the row IDs through a new read connector and compares them in a validator.

Tests (failed first: `policyengine_api.services.v2.countries.validators`
did not exist): the comparison in every direction; the service against
SQLite; the startup function's selection, each failure that stops startup,
and the logged skip without the error's detail; importing the ASGI module
with an unusable v2 runtime URL failing without printing the password; and,
against migrated disposable Postgres as the runtime role, the check passing
on the revision's rows and failing once an extra row exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review found that the startup check sorted its failures by exception type
in a way that stopped startup for a brief Secret Manager outage (reported
as unusable v2 configuration) while only logging rejected credentials (a
connection error). The check now stops startup only when the database
answers and the answer disagrees with the country list: different
countries, a missing table, or no privilege to read it. When it cannot get
an answer, because the v2 configuration cannot be used, the secret cannot
be resolved, or the database cannot be reached or queried, it logs why and
startup continues; the readiness check already reports unusable v2
configuration as not ready and recovers once the secret resolves.

The rule `api.startup.no-import-side-effects` now says what deployed
startup does: it opens no network connections unless the process runs on
Cloud Run or has a v2 runtime database configured. The country list and
the startup check join the mypy file list and rule `api.quality.mypy-v2`.
The country list's docstring and the Alembic skill now say that a deploy
runs a country-adding revision before the new release starts, so the
previous release refuses to start afterwards, and list the US-and-UK
checks a new country must also widen.

Tests (failed first: configuration errors and `InterfaceError` were
raised, and importing the ASGI module with an unusable v2 URL exited with
an error): configuration and secret failures are logged with their
message and startup continues, connection and interface errors are logged
by type only, and importing the ASGI module with an unusable v2 URL logs
the skipped check without the password.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a full test run, the tests that run Alembic commands load Alembic's
logging configuration, which disables every logger that already exists,
including the startup check's. Its four log tests then captured nothing
and failed, though they passed on their own. An autouse fixture now
re-enables the check's logger for each of its tests.

Tests (failed first: running the Alembic unit tests before the startup
check tests failed all four log tests); with the fixture, both files pass
together (46 passed).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nto v2plan/2b-204-household-calculations

# Conflicts:
#	docs/engineering/migration-contracts.md
#	docs/engineering/skills/alembic-migrations.md
#	docs/generated/migration_contracts.json
#	policyengine_api/data/v2/catalog/extraction.py
#	policyengine_api/data/v2/migration_target.py
#	policyengine_api/data/v2/models/associations.py
#	policyengine_api/fastapi_routes/dependencies.py
#	policyengine_api/query_parameters.py
#	tests/integration/test_alembic_v2_lifecycle.py
#	tests/unit/test_migration_contract_artifacts.py
#	tests/unit/v2/test_alembic_v2.py
#	tests/unit/v2/test_models.py
…eck' into v2plan/2b-204-household-calculations
…rvice

Catalog versions gain spm_availability: whether the version offers SPM
settings, with their JSON schema and the certified defaults, or
{"available": false}. After publishing, catalog initialization reads the
simulation service's public versions answer and records it on each
published version that has none yet; a US version offers SPM when the
service lists a canonical capability with pinned defaults for its
PolicyEngine.py version, and other countries never do. A bundle pins its
SPM release, so a recorded value never changes, and versions published
earlier get it on the next run. The publication step now receives
SIMULATION_ENTRYPOINT_URL. The three database steps of initialization are
grouped as PublicationSteps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model catalog result gains spm: available, and when it is, the JSON
schema of a household's SPM selection and the certified defaults. A version
whose availability is not recorded yet, or that does not offer SPM, is not
available. The route contract and the stage 9 guide name the field.

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

A stored v2 household becomes the situation shape the simulation
service's household functions take: people and each group collection by
its plural, each group listing the people whose memberships name it, every
record named by its ID so a calculated value maps back to its record. The
saved SPM choice is read separately. A v2 policy's values become parameter
values keyed by YYYY-MM-DD.YYYY-MM-DD period ranges, with an open end as
2100-12-31.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /v2/household-calculations checks the country's countries row allows
household calculations (422 naming the country otherwise), reads the stored
household and optional policy through the households and policies services,
selects the country's running catalog version and sends the situation, saved
SPM choice, policy parameters and options (axes, add_missing_variables) to the
Simulation Entrypoint. GET /v2/household-calculations/{calculation_id} reads
the calculation back with its household, warnings, spm_config,
spm_provenance and error. Neither route needs an account.

The simulation service is called through the generated simulation client,
one per process, with the gateway credentials and correlation headers; a
missing or partial configuration, an unreachable service, an error answer or
a refusal is a 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The parallel live staging job now runs a submit, poll and result cycle for a
new US household; six calculations submitted at once against submit and
finish time limits; a household across 401 axis points, which must finish;
and a parity set of two US and two UK households (one of each with axes)
compared value by value with API v1's /calculate on the same candidate. The
job's time limit rises from 20 to 30 minutes for the Modal calculations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A 422 from the simulation service (a household or options it finds invalid,
such as a group no person belongs to) now answers 422 with each invalid field
and why, instead of 503 without a reason. A body that is not JSON or not of
the documented shape, and a gateway token that cannot be fetched, now answer
503 instead of 500. Each answer is decoded once, by the generated client.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The axes of one group must share one point count, and the groups' point
counts may multiply to at most 10,000, as the simulation service requires.
Such requests now get 422 from the Public API before anything is read or
sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request no longer takes `year`: the calculation's year is the
household's default year, the year its values name, so a calculation is
never labelled with a year it did not calculate. It no longer takes `force`
either, so callers without an account cannot skip the 28-day reuse of a
succeeded calculation and start a new Modal call for each request. The
stage 11 guide notes the year the household function uses when it adds
missing variables.

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

A household calculation's catalog reader, with its database session, is now
opened only once a submission needs the catalog version, and closed after
it; polls and refused submissions open none. The situation's group
collections come from the households service's own lists, and the live
checks share one helper for their run-specific amounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog publication recorded {"available": false} for a US version the
simulation service's /versions did not serve yet, and a recorded value is
never replaced, so the version could never offer SPM settings. Such a
version now stays unrecorded and a later publication run records it. The
stage 9 runbook lists SIMULATION_ENTRYPOINT_URL for the publication command,
and the catalog cache note says how long an earlier answer may be reused.

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

The parity set gains a US household under a stored reform that also adds
missing variables (compared with /calculate-full) and a UK household under a
stored reform, so the policy conversion and the added variables are checked
against API v1 too. The memory check's description now says what it asks
for.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The generated client's models wrote the household with its groups before
its people and each group's members after its values, in the request and
in the answer. The household function lists warnings and calculated values
in the household's key order, as API v1 does, so the order could differ
from API v1's. The submission and the answers are now checked against the
generated models but sent and returned as built and decoded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default service factory built the simulation client before any check,
so with the simulation service unconfigured a country without household
calculations, or a household that does not exist, got 503 instead of 422
or 404. The client is now built only when a submission or read is sent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An axis without a period varied the country model's default period, the
current year, rather than the year the household's values name. Such an
axis is now sent with the household's default year as its period.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/v2/household-calculations requests were logged with the route group
"unknown"; they now belong to the household group. The contract registry
records both routes as a typed v2 workflow, and the generated migration
contract documents include them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Catalog publication now stops before extraction when
SIMULATION_ENTRYPOINT_URL is unset, instead of after the catalog is
written. When the simulation service's /versions answer cannot be read,
publication logs a warning and leaves SPM availability unrecorded for the
next run, so a slow or unreachable service no longer stops a release. The
cache note and the stage 9 guide say that an instance keeps an earlier
answer until it restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service answers 503 with Retry-After while its database or
version manifest cannot be read. The live staging checks retried such
answers when polling but not when submitting, so one short outage failed
the job that staging promotion waits on. Submissions are now sent again,
after the Retry-After wait, until the calculation's deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Requests to /v2/household-calculations were logged with the household
route group's v1 database flags and its configured compute backend. They
now log a Supabase read for a submission, no database read for a poll, no
write, and the Cloud Run Simulation Entrypoint's v2 workers as the compute,
whatever those flags select.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The household client held its HTTP client inside the generated
AuthenticatedClient with an empty token but never called a generated route;
it now keeps the HTTP client itself. Its description says the household's
key order is the stored household's as the v2 database returns it. The SPM
availability description and changelog entry say a version gets its record
on the next publication run of its own PolicyEngine.py version, and that
only an unreadable versions answer never fails publication.

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

The live household checks sent a request again after any 5xx answer until
the calculation's 600-second deadline, so a lasting failure kept each check
waiting until the job's time limit and lost the failing answer. They now
retry 502, 503 and 504 for at most 90 seconds in a row and fail at once,
with the answer's body, on anything else. The parity set compares warnings
regardless of order, since the v2 database orders stored keys itself, and
one US household now has a deprecated input so that both calculations warn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 2 commits October 28, 2026 00:41
An invalid SIM_ENTRYPOINT or SIM_COMPUTE_HOUSEHOLD, which household
calculations never read, reduced their migration log record to a flag
error. The record is now built from what the route uses, checking only the
route implementation. The guide and the live check helper say which
requests are sent again after a 502, 503 or 504, and that failures the
Public API answers with 503 take about 90 seconds to fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper that reads a simulation service answer took a generated model
class as Any, with an ANN401 suppression; it now takes the model's
from_dict reader, typed by the model it returns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service now reads the stored household, the policy's
parameter values and the catalog version from the Public API's tables
itself. A household calculation's submission therefore carries only the
country, the household's and policy's IDs, the options and the running
catalog version's PolicyEngine.py version. The Public API still checks the
country allows household calculations and that the household and policy
exist in it, and fills an axis's missing period with the household's
default year. The conversion of stored households and policies, which moved
into the simulation service, is removed here.

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

The household function now takes the run's year, the household's default
year, from the simulation service and uses it for an axis without a period,
so the Public API no longer fills that period itself. The stage 11 guide
says the household's default year is also the year of the variables
add_missing_variables adds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 3 commits October 28, 2026 17:49
The simulation service now refuses the caller's data with 422 and its
reason as text: a policy changing a parameter the catalog version lacks, or
a stored policy value or household the household functions cannot take,
such as a group no person belongs to. The Public API read only a list of
refused fields, so such a refusal became 503. It now reads the service's
refusal model and answers 422 with the reason, or with each refused field.
The service's other refusals (400), for references the Public API has
already checked, stay 503. The route's declared 422 answer, the adapter's
description and the stage 11 guide say so.

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

The staging parity household that adds missing variables named its person
`adult`, so API v1's /calculate-full added them for the current calendar
year while the v2 path adds them for the household's default year, and the
two would differ from 2027. It now names its person `you`, whose age gives
/calculate-full the household's year, and a test with the date fixed in
2027 checks both sides use the same year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A test now sends an axis with an explicit period and one with a null
period and checks both reach the simulation service unchanged, as they must
now that the household function fills a missing period itself. The check
that a household has a default year returns nothing, so it is named
require_default_year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 2 commits October 29, 2026 02:22
The generated refusal model keeps a 422's detail as it came when it is
neither text nor a list of field errors, such as null, an object, a number,
a list of strings or an entry without a type, and building the reason from
it raised, so the route answered 500. Such a refusal now means the
simulation service's answer could not be read, and the route answers 503 as
documented.

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

The declared 503 answer now lists what is left for it (an unavailable
dependency, an unreadable answer, a refused reference the Public API has
already checked, or no v2 worker), and the stage 11 guide says a refused
request field gets 422 with each field. The parity year test checks the
default year the live check stores the household with, not a constant, and
the axis-period tests are one parametrized test.

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

This branch was successfully deployed

1 active deployment
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.

1 participant