Repository navigation
Conversation
…nerated client Economy reports live in the Simulation Entrypoint: it creates their simulations, reports and runs, and stores their results. The Public API reaches them through `EconomyReportSimulation`, a small protocol in the new `services/v2/economy_reports/types.py`: submit a report, read one by ID or by v1 report number, run one again, list its runs, and read a simulation. Its records are plain dataclasses, and its three failures say what happened: the record does not exist (404), the entrypoint cannot run the request (422, with its reason), or the entrypoint cannot be used now. `libs/simulation_economy_reports.py` implements the protocol with the client generated from the entrypoint's OpenAPI document (`policyengine-api-simulation-client`), and is the only module that imports it. Requests go to `SIMULATION_ENTRYPOINT_URL` with the existing gateway bearer token and the request's correlation identifiers. The generated client parses only the success answers the entrypoint's OpenAPI document describes, and raises on the entrypoint's own refusals, whose `detail` is text: a response hook turns every error answer except a 401 (which the gateway auth retries with a new token) into the protocol's errors before the generated parser runs. Network failures, other answers and answers that do not parse mean the entrypoint is unavailable. The client is not a dependency yet: no release contains the economy report routes. Nothing imports the adapter at module level (it joins the banned module-level imports), so the application starts without the client. Tests (failed first: the adapter module did not exist): against the locally generated client and an in-memory transport answering as the entrypoint does, the request bodies and paths of all six routes, a succeeded report read back with its exact result (a real congressional district result as the entrypoint serializes it), a migrated report with no run, each error answer, a network failure, a token refreshed after a 401, the request ID header, and the URL and gateway auth read from the environment. They skip when the client is not installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ountries `POST /v2/reports?country_id=us` takes an economy report: its year, a region code of the current catalog version (`state/ca`, `country/wales`), an optional dataset name, and the baseline and reform, each a v2 policy and dynamic (an empty object means current law). The simulation service creates the two simulations, the report and its first run, starts the run, and the route answers 201 with the report and that run. The run uses the current catalog version: the one published for the PolicyEngine.py version the Public API runs, which its v2 metadata routes also serve by default. Before the simulation service is asked, the country's `countries` row must allow society-wide calculations; a household-only country (or one with no row) gets a 422 naming it, and nothing is created or sent. The simulation service's own refusals (no catalog for the version, no such region or dataset, a region below the country without a household filter, an unknown policy or dynamic) are 422s with its reason. An unusable simulation service or v2 database is a 503, logged with its reason; anything else is a secret-safe 500. The body must name the query's country (400), has no options yet and is limited to 16 KiB. No user credential is needed: a user keeps a report through `/v2/user-reports`. The route follows the v2 layout: a `reports` route package calling one method of a new economy report service, which reads the countries row through its own read connector (the runtime role already holds SELECT on `countries`). The default service uses the generated-client adapter and the v2 database, both loaded only when a request needs them; without the client installed the route answers 503. Requests are logged under the `report` route group, the contract registry gains the `economy_reports_v2` workflow, and its generated documents are refreshed. Tests (failed first: the service and route tests could not import the service's session module and the default adapter factory): the service against SQLite with a test-only household-only country row (`zz`, only `household_enabled` set) and a country with no row, both refused before the fake simulation service is called; the submission sent on the current version; and, through the routes on the real service, the 201 body, the country mismatch, eleven invalid bodies and four missing fields, the household-only 422 naming the country, the relayed refusal, the 503s, the 500, the missing client and the OpenAPI document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t run
`GET /v2/reports/{report_id}?country_id=us` returns the report, its newest
run and, once that run succeeded, its result in today's economy result
format. `GET /v2/reports/by-legacy-number/{number}?country_id=us` finds the
report migrated from a v1 report output number and answers the same way. A
report of another country, or one the simulation service does not have, is
a 404 naming the report and country.
A report with no run, such as one migrated from v1, is calculated the first
time it is read: the read starts a run with the `system` trigger on the
current catalog version, without forcing new simulation runs, then reads the
report again so the answer shows the new run as the simulation service
stored it. The run is not started when the country's `countries` row no
longer allows society-wide calculations, and a run the simulation service
refuses (for example, a region the current catalog version lacks) is logged
and leaves the report not run, so the read still answers.
Tests (failed first: the service could not import the not-found error, and
the routes answered with the catch-all 404 and 405): the service reading a
report as stored, unknown and other-country reports, the first run started
with the system trigger and current version then read again, no run for a
report that has one, none for a report of the test-only household-only
country, a refused first run, and the v1-number lookup found and not found;
and through the routes, the succeeded result, the first run, the 404s, seven
reads outside the contract, and the OpenAPI document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`POST /v2/reports/{report_id}/runs?country_id=us` runs the report again on
the country's current catalog version with the `manual` trigger. The body
is `{}` to reuse the simulation runs that already exist for that version and
year (most often the current-law baseline), or `{"force": true}` to
calculate both simulations again as new attempts; `force` takes only a JSON
boolean. It answers 201 with the new run. As on submit, a country whose
`countries` row does not allow society-wide calculations gets a 422 naming
it before the simulation service is asked; otherwise the report must be the
country's (404), and the simulation service's refusals are 422s with its
reason.
`GET /v2/reports/{report_id}/runs?country_id=us` lists every run of the
report, newest first, each with its trigger, status, simulation runs,
comparison step, and the catalog version (`tax_benefit_model_version_id`)
and PolicyEngine.py version it ran on.
The route builder's operations become module-level response builders called
from each route, which keeps the builder within the complexity limit.
Tests (failed first: the service had no re-run or run list, and the routes
answered with the catch-all 405): the re-run's version, trigger and force
flag, the household-only refusal with no call to the simulation service,
unknown and other-country reports, a relayed refusal, and the runs listed
newest first; and through the routes, the 201 body, reuse unless forced, the
household-only 422, four bodies outside the contract, the run list, an
unknown report and the OpenAPI document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`GET /v2/reports?country_id=us&report_id=<id>&report_id=<id>` lists the country's reports among the given IDs (one to 25, the repeated `report_id` key), in the order asked and once each: each report's simulations, status and newest run, without its result, so a page of saved reports can show them without loading every result. Unknown reports and other countries' are left out. Listing does not start the first run of a report that has none; reading the report does. Each listed report is one read from the simulation service, which has no list route, hence the limit of 25. The report item splits into `ReportSummaryItem`, which the list returns, and `ReportItem`, which adds `result` for single reads and submissions. Tests (failed first: the service had no listing, and the route answered with the catch-all 405): the order kept, duplicates, unknown and other-country reports dropped, and no run started; and through the route, summaries without results, five queries outside the contract (no IDs, a bad ID, no country, two countries, 26 IDs), and the OpenAPI array parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lation service
`POST /v2/user-reports` now takes a v2 `report_id`, a v1
`legacy_report_id`, or both (at least one, as saved policies already do),
and `PATCH /v2/user-reports/{id}` can repoint a saved report to a v2
`report_id` (not clear it). The v2 tables keep no foreign key to the
simulation service's reports, so before a `report_id` is saved the service
is asked for that report, outside the v2 transaction, as the v1 reference
already is: an unknown report, or one of another country, is a 422 naming
it, and an unusable simulation service is a logged 503 with nothing saved.
A shared report (`/v2/shared-reports/{id}`) now shows its `report_id`.
A new `simulation_refs` service holds the checks (`require_report`, and
`require_simulation` for saved simulations next), behind the
`SimulationReferenceChecker` protocol the association services take. The
default builds the simulation client only when a request checks a v2
reference, so the other saved-record routes do not depend on it, and its
module joins the banned module-level imports. The contract registry records
`report_id` as a patch field and a shared-report field.
Tests (failed first: the reference package did not exist, and the shared
read and route tests could not build their records): against SQLite and the
fake simulation service, saving by v2 ID, unknown and other-country reports
refused with nothing saved, both references checked, repointing, the
unavailable service, a service built without the checker, the required
reference, and the shared report's `report_id`; and through the routes,
create by v2 ID or both, neither refused, repoint, null refused, and the 422
and 503 mappings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the simulation service
`POST /v2/user-simulations` now takes a v2 `simulation_id`, a v1
`legacy_simulation_id`, or both (at least one), and
`PATCH /v2/user-simulations/{id}` can repoint a saved simulation to a v2
`simulation_id` (not clear it). Before a `simulation_id` is saved, the
simulation service is asked for that simulation, outside the v2
transaction: an unknown simulation, or one of another country, is a 422
naming it, and an unusable simulation service is a logged 503 with nothing
saved. The check is the reference service's `require_simulation`, and the
contract registry records `simulation_id` as a patch field.
Tests (failed first: a v2 `simulation_id` was refused with 422 and the
service had no check): against SQLite and the fake simulation service,
saving by v2 ID, unknown and other-country simulations refused with nothing
saved, repointing, and the required reference; and through the routes,
create by v2 ID, repoint, null and missing references refused.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…time role A new integration test migrates the disposable Postgres database, adds a test-only countries row (`zz`, with only `household_enabled` set; no real country is household-only yet) as the owner, and runs the economy report service as the Public API's runtime role: submitting or re-running a report for that country is refused with the message naming it, and the fake simulation service is never called. Through the route, a society-wide country's submission reads its row as the runtime role and reaches the simulation service. The test-only row is removed afterwards, so later tests see only the listed countries. The cross-database integration workflow runs the new test in the step with the countries list, and the workflow test checks that it does. The step's environment is unchanged: the job's literal disposable database URLs. Tests (failed first: the workflow test did not find the new test in the workflow): locally against a disposable postgres:17 container, migrated and granted as the workflow does, the two integration tests, alongside the countries integration tests in one session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e fetched The gateway bearer auth fetches its token inside httpx's auth flow, and raises `GatewayAuthError` when Auth0 cannot be reached or refuses, or the client secret cannot be read from Secret Manager. The economy report adapter converted only httpx and parsing failures, so that error escaped: every `/v2/reports` route, and every save of a v2 report or simulation ID, answered a 500 instead of the 503 the protocol promises when the simulation service cannot be used. The adapter now converts it to `SimulationServiceUnavailableError`, with the reason, before any request is sent. Tests (failed first: `GatewayAuthError` escaped the adapter): an auth that cannot fetch a token makes a read unavailable and sends nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ckage The protocol for the Simulation Entrypoint's economy report routes, the records it answers, its two run enums, the submission and simulation-choice inputs, and its three errors move from `services/v2/economy_reports/types.py` to a new `services/v2/simulation_entrypoint/types.py`. The economy report routes and service use them, and so do the checks of saved v2 report and simulation IDs, the saved-record error mapping and the generated-client adapter, none of which now depends on the economy report package. That package keeps only its request types. Docstrings that said "API v1" now say "the Public API". Tests (failed first: four modules imported the economy report package): no saved-record, reference-check, error-mapping or adapter module imports it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion client cannot be built The default economy report service built the simulation service adapter before it read the country's `countries` row, so when the generated client was missing or `SIMULATION_ENTRYPOINT_URL` or the gateway auth was unusable, a household-only country's submission or re-run answered 503 instead of the 422 that names the country. Both default factories now pass a `DeferredEconomyReportSimulation`, which builds the adapter at its first call to the simulation service (and tries again if that build fails). The economy report service therefore reads the `countries` row and refuses a household-only country without the client or its settings; a society-wide country still gets the 503 when the adapter cannot be built. The saved-record reference checks use the same deferred adapter, which replaces their own deferred wrapper. The new module joins the banned module-level imports, like the other services the default factories load. Tests (failed first: building the default service raised the client's unavailability before the countries check): with the client import failing, the default service refuses the test-only household-only country on submit and re-run and reports the simulation service unavailable for the US; and through the route with the default factory, a household-only submission is a 422 naming the country. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… both simulation clients The economy report adapter repeated three pieces of the Simulation Entrypoint client of the v1 routes, and imported one of its private helpers, because that module builds a client when it is imported. They now live where both import them without side effects: - `libs/simulation_http.py`: `required_base_url`, which reads a base URL from the environment and refuses one that is missing or not an absolute HTTP(S) URL free of credentials, a query and a fragment; and `attach_request_identifiers`, the request hook that sends the API request's correlation identifiers. - `gateway_auth.gateway_bearer_auth_from_environment`, which returns the gateway bearer auth, None where auth may be left unset, or raises `GatewayAuthError` when it is partly set or required but missing. The v1 client keeps its behavior and messages, including its warning when it runs without auth; the adapter converts the two errors to the simulation service being unavailable, as before. Tests (failed first: the shared module and function did not exist): the base URL read, missing and five unusable forms; the identifiers sent inside and outside an API request; and the gateway auth selected when complete, absent where not required, and refused when required or partly set. The existing v1 client, service and route tests pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A saved report holding both a `report_id` and a `legacy_report_id` records that the v2 report was migrated from that v1 report output, but each reference was only checked to exist, so a caller could pair two unrelated reports. Now, when a saved report would hold both, the simulation service's report must record that it was migrated from that v1 report output; otherwise the request is refused with the existing "references contradict each other" error (400) and nothing is saved. On create, both supplied references are checked together. On `PATCH`, a new reference is checked together with the one the saved report keeps: the service reads the saved report's references first, checks them outside the v2 transaction as before, then refuses the update with a 409 if the saved report changed meanwhile. A `PATCH` that supplies neither reference asks the simulation service nothing. The documentation now describes v2 IDs as accepted: the user-login migration document's shared-report item gains `report_id`, its deferred item becomes a section on v2 report and simulation IDs, and the saved report and saved simulation model docstrings, the create fields and the route description say what each reference is and how they pair. Tests (failed first: an unrelated pair was saved, and a changed report was repointed): against SQLite and the fake simulation service, a v2 report migrated from another v1 report and one never migrated are both refused on create; on patch, a new v2 or v1 reference that does not match the stored one is refused and a matching one pairs them; a rename asks nothing; and a saved report changed during the check is not repointed. The routes map the two refusals to 400 and 409. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rated client
The simulation service's OpenAPI document now declares its error answers,
so the client generated from it parses them: 404 and 503 as
`EconomyErrorResponse` (`{"detail": "<text>"}`), 422 as
`EconomyRefusalResponse` (its reason as text, or FastAPI's refused fields),
and, on the two report reads, 500 when a succeeded run's stored result
cannot be read back. The adapter now turns each parsed answer into the
protocol's error, and the response hook that read error answers before the
old client's parser is removed:
- 422: the request is refused, with the service's reason or each refused
field as `location: message`;
- 404: the record does not exist, except a 404 whose detail is exactly
"Not Found", which is FastAPI's answer for a route the service does not
serve (such as a revision without these routes) and so means the service
cannot be used; the service's own 404s name the missing record;
- 500 on a report read: a new `SimulationRecordUnreadableError`, answered
502 by the report routes and by saving or repointing a v2 report, which
declare it in OpenAPI;
- 503, undeclared answers, answers that do not parse, network failures and
a gateway token that cannot be fetched: the service cannot be used (503).
Tests (failed first: the new error did not exist, then a "Not Found" route
answer was read as a missing record and the declared 500 as unavailability,
then the routes answered 500 for the unreadable report and the OpenAPI
document lacked the 502): against the client generated from the service's
current document, every declared 404 on the five routes that have one, the
unserved-route 404 on reads and submit, the declared 500 on both report
reads, and the earlier refusal, unavailability, token and request-ID cases;
and through the routes, the 502 answers and their OpenAPI declarations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two reads of a report that has never run, such as one migrated from v1, can arrive together; both see no run and both ask the simulation service for a first run. The service now answers a `system` run of a report that already has one with that newest run, under a lock, so it records a single run. The Public API needs no change for this, because it starts first runs only with the `system` trigger and no force, and answers with the report as the service then holds it. A new test pins that: two simultaneous first reads start one run between them, and both answer with it. The test's fake simulation service follows the service's new rule for `system` runs. Tests (failed first: with a fake that adds a run for every request, as the service did before, the two reads recorded two runs): two threads read the same never-run report, each waiting until both have read it, and the fake records one pending run, which both answers show. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… report can run again Re-running a report, listing its runs and saving its v2 ID first checked that the report is the country's by reading the whole report from the simulation service, which rebuilds its stored result. When that result cannot be read back, the service answers 500, so the Public API answered 502 to the re-run that would make the report readable again, to listing its runs, and to saving it, though none of them uses the result. Listing chosen reports also answered 502 for the whole request when one of them was unreadable. The country now comes from the report's simulations: its runs name them, and a simulation's country is the report's. A report with no run has no stored result, so it is read whole as before. `read_report_country`, beside the deferred adapter, does this for the report service and the saved-report check; the check still reads the whole report when it must compare the v1 report the report was migrated from. Listing leaves out a report whose result cannot be read back, as it leaves out unknown ones, and logs it; the caller can tell from the IDs it asked for. These checks also no longer make the service rebuild results nobody reads. The shared module is imported at module level and leaves the banned module-level imports. Tests (failed first: re-running, listing the runs of, and saving an unreadable report each reached the full report read, and a listing including one failed): with the fake simulation service, which now gives each report its own simulations and runs, and can fail to read a report's result; and the re-run of a report of another country that has runs is not found. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mulation A saved simulation holding both a `simulation_id` and a `legacy_simulation_id` records that the v2 simulation was migrated from that v1 simulation, as a saved report holding both IDs does, but only saved reports checked the pair. A saved simulation now gets the same rule: when it would hold both, the simulation service's simulation must record that it was migrated from that v1 simulation, or the request is refused (400) and nothing is saved. A `PATCH` setting one reference is checked together with the one the saved simulation keeps, and is refused (409) if the saved simulation changes during that check; a `PATCH` setting neither asks the service nothing. The two services now share the pieces: `RecordReferences` (a saved record's v2 and v1 IDs), `repointed_references` and `require_unchanged_references` in the associations package, and a `require_migrated_from` that names the kind of record. The reference service's `require_simulation` takes the v1 ID as `require_report` does. The saved-simulation docstrings, route description, migration document and changelog describe the rule. Tests (failed first: an unrelated pair was saved on create and on patch, and a simulation changed during the check was repointed): a v2 simulation migrated from another v1 simulation and one never migrated are both refused on create; on patch, a new reference that does not match the stored one is refused, a matching one pairs them, and a rename asks nothing; and a simulation changed during the check is not repointed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Repointing a saved report or simulation checks its references with the simulation service before the update's transaction opens, then refuses the update if the stored references changed meanwhile. That comparison read the row without locking it, so two updates of one saved record could both pass it before either committed: under Postgres READ COMMITTED the second waited for the first's row lock and then wrote over it, leaving a v2 and v1 pair that was never checked together. An update that repoints a saved record now reads the row FOR UPDATE, so updates of one saved record take turns and each compares against the row as the one before it left it; the later one is refused with the 409 the check already gives. A rename still takes no lock. The runtime role already holds UPDATE on both tables, which a locking read needs. The cross-database integration workflow runs the new test with the signed-in identity tests, and the workflow test checks that it does; the step's environment is unchanged. Tests (failed first, against migrated Postgres as the runtime role: while another connection held the row and changed its v1 reference, the update did not raise and wrote the unchecked pair): for a saved report and a saved simulation, the update waits for the other connection's lock, sees the changed row, is refused with a conflict, and leaves the other change in place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…happen Every `/v2/reports` route declared the same eight error answers, including ones it cannot give: a 400 and a 413 on reads, a 404 on submit and listing, a 502 on routes that never read a stored result. Each route now declares the three every route can give (422, 500, 503) and its own: submit 400 and 413; the two reads 404 and 502; a re-run 404 and 413; the run list 404. A report route's unexpected failure was logged by its error type only. It is now logged, as the saved-record routes' failures already are, with the locations of its innermost frames, never its message. The shared `failure_log_attributes` in the v2 errors module builds those attributes for both. The simulation-service contract's module docstring now counts its four errors. Tests (failed first: every route declared the same eight answers, and the logged failure had no frame locations): the exact declared answers of each of the six operations, and an unexpected failure's log record naming where it was raised without its message. The test re-enables the routes' logger, because Alembic's logging configuration in earlier tests disables loggers that already exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r failures as errors Every report route described its 422 as a refusal of the report itself (a country without economy reports, or a report the simulation service cannot run), but only submit and re-run can refuse that way; the reads, the list and the run list answer 422 only for a request outside the schema. Those two routes now declare the longer description and the others the plain one. A report route's unexpected failure, or the simulation service being unavailable, was logged as a warning while the saved-record routes log the same kind of failure as an error, so a filter on error severity missed report-route 500s. They are now logged as errors. The route-facing protocols of saved reports and simulations still described saving only v1 records; their docstrings now say what each reference is, how the two pair, and when an update is refused as a conflict. Tests (failed first: four routes described the report refusal, and the failure was logged as a warning): each route's 422 description, and the logged failure's severity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## v2plan/1g-126-countries-table #4108 +/- ##
=================================================================
- Coverage 87.80% 87.38% -0.43%
=================================================================
Files 270 286 +16
Lines 16333 17144 +811
Branches 2653 2760 +107
=================================================================
+ Hits 14341 14981 +640
- Misses 1306 1461 +155
- Partials 686 702 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
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 #4107
Summary
/v2/reportsfor economy (society-wide) reports. The simulation service creates, runs and stores them; the Public API calls its economy report routes (Run and store v2 economy reports in the Simulation Entrypoint policyengine-sim-api#974) through the generatedpolicyengine-api-simulation-client.POST /v2/reports?country_id=creates the baseline and reform simulations (the region by its catalog code), the report and its first run (triggerinitial) on the country's current catalog version: the one published for the PolicyEngine.py version the Public API runs, which its v2 metadata routes also serve by default.GET /v2/reports/{report_id}andGET /v2/reports/by-legacy-number/{number}read a report with its newest run and, once that run succeeded, its result in today's economy result format. A report with no run, such as one migrated from v1, is calculated the first time it is read (triggersystem); the simulation service records one run when several first reads arrive together.GET /v2/reports?country_id=&report_id=…lists 1 to 25 chosen reports, in the order asked, without their results, and starts no runs. The simulation service has no list route, so each listed report is one read.POST /v2/reports/{report_id}/runsruns a report again on the current catalog version (triggermanual), reusing the simulation runs that exist for it unless{"force": true}.GET /v2/reports/{report_id}/runslists the runs, newest first, with the catalog version and PolicyEngine.py version each used.countriesrow does not allow society-wide calculations (a household-only country) gets a 422 naming it on submit and re-run, before anything is sent to the simulation service, even when the simulation client or its settings are unusable.country_idquery; another country's report is a 404. No user credential is needed: a user keeps a report through/v2/user-reports./v2/user-reportstakes a v2report_idand/v2/user-simulationsa v2simulation_id, alone or with the v1 ID, on create and onPATCH. The tables keep no foreign key to the simulation service's records, so each v2 ID is checked with the simulation service first, outside the v2 transaction. A saved record holding both IDs must name the v2 record migrated from that v1 record (400 otherwise); aPATCHthat repoints a saved record locks its row and is refused (409) if it changed during the check. A shared report (/v2/shared-reports/{id}) now shows itsreport_id.libs/simulation_economy_reports.py, which implements a small protocol (services/v2/simulation_entrypoint/types.py) that everything else uses; services and routes are tested against a fake of it. The adapter reads the simulation service's declared answers: 404 (no such record; a 404 whose detail is exactly "Not Found" is FastAPI's answer for a route the service does not serve, which means the service cannot be used), 422 (its reason), 500 on a report read (its stored result cannot be read back, answered 502) and 503. Network failures, a gateway token that cannot be fetched and undeclared answers mean the service is unavailable (503).Migration work, as the PR skill asks:
reportroute group gains thereportssegment; the contract registry gainseconomy_reports_v2, anduser_report_associations_v2anduser_simulation_associations_v2gain the v2 ID fields. Generated migration contract documents are refreshed./{country}/economy/…,/{country}/reportand/{country}/simulation, is unchanged. Nothing calls/v2/reportsyet./v2/reportsroutes;report_idandsimulation_idaccepted by the saved-record routes, with their new 400, 409, 502 and 503 answers;report_idin the shared report.report_idandsimulation_idcolumns already exist.Waits for before merging
policyengine-api-simulation-clientthat contains the economy report routes (an operator step). Until it is a dependency, every/v2/reportsroute, and saving a v2 ID, answers 503 when deployed. That release enables these follow-ups:pyproject.tomlanduv.lock;libs/simulation_economy_reports.pyandlibs/simulation_http.pyto mypy's file set (with the matching rules-registry change);pytest.importorskipfrom the adapter tests so CI runs them;Environment variables
No new variables. The new code reads:
SIMULATION_ENTRYPOINT_URL: GitHub environment secret (staging,production) → jobenv:of the Cloud Run deploy jobs inpush.yml→deploy_cloud_run_candidate.sh, which adds it to--set-env-varswhen it is not empty → Cloud Run runtime. The economy report routes exist only on the Cloud Run Simulation Entrypoint, so they use this URL whateverSIM_ENTRYPOINTselects (it iscloud_run_simulation_entrypointin both environments).GATEWAY_AUTH_ISSUER,GATEWAY_AUTH_AUDIENCE,GATEWAY_AUTH_CLIENT_ID,GATEWAY_AUTH_CLIENT_SECRET_RESOURCE: organization secrets → the deploy step'senv:→ the deploy script, which also setsGATEWAY_AUTH_REQUIRED=1→ Cloud Run runtime. The client secret is read at runtime from Secret Manager by the Cloud Run runtime service account. These are the values the existing Simulation Entrypoint client already uses.countriesrow (SELECT already declared for the runtime role).Not verified: that the Auth0 client the gateway token comes from holds
simulation:submitandsimulation:readfor the simulation service's audience (the existing calls to/simulate/economy/comparisonand/jobs/{id}need the same two permissions), and that each environment'sSIMULATION_ENTRYPOINT_URLserves a simulation service revision with the economy report routes.The integration workflow change adds two test modules to existing steps; their database URLs are the job's literal disposable values.
Inline suppressions
None added.
Testing
countriesrow with onlyhousehold_enabledset) and a fake simulation service; the saved-record reference checks; the adapter against a client generated locally from the simulation service's current OpenAPI document, through an in-memory transport (these skip when the client is not installed).pytest tests/unit tests/contractpasses.ruff format --check .,ruff check .,mypy,uv lock --check,scripts/run_quality_guards.py,scripts/export_migration_contracts.py.Notes for reviewers
force: trueneeds no credential and has no rate limit, like the v1 economy routes.🤖 Generated with Claude Code