Repository navigation
Conversation
A report gains an optional budget window size: an economy report with one compares its two simulations in each year of the window. Each calculation of such a report is a budget-window run (new table simulation.budget_window_runs) on one catalog version, which records its trigger, whether its years calculate their simulations again, how many years it calculates at once, its status, when the safety check next looks at it, and the lease of the caller starting its next years. Each year is a report run that names its budget-window run, of the same report and catalog version, with at most one per year. BudgetWindowRunStore creates, claims, releases and finishes runs, and reads each year's runs and budget rows; nothing stores a total. Revision 0005 adds the column, the table and the link; its downgrade removes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EconomyReports gains three public steps the budget-window path needs: prepare_report creates a report and its simulations without running it, optionally with a budget window; report_setup reads a report's simulations for a catalog version; run_year finds or creates both simulation runs and a report run for one year, recorded as one year of a budget-window run even on the system trigger, and starts what can start, with the same reuse, forcing and labour supply rules as a single-year report. submit and run_report use them unchanged. A budget-window report is not a single-year economy report: run_report and the economy report reads answer that no such economy report exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… all succeed BudgetWindowReports submits a budget-window report and its first run, and runs one again (manual, or system, which returns the report's newest run). Each run starts its years through EconomyReports.run_year in year order, at most max_concurrent_years (five by default) at once, so a US national window of up to 75 years cannot take every segment container. A caller first claims the run for a two-minute lease, so two callers never start the same year; it releases the claim with the run's next safety-check time, and tries again if a year finished while it held the claim. The next years start when a reported call frees a year's place (start_years_after_call) and on each safety-check pass (resume_waiting_work), so a run whose caller stopped carries on. The first failed year fails the run with that year's error, and a year that cannot start fails it as invalid input; years already running keep their results. Once every year has succeeded the run has succeeded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d on read A budget-window report reads with its newest run, and a run with every year of its window: a started year has its report run's status, stage and error; a year not started yet is queued, and one a finished run never started was not run. A run has failed as soon as one year has, before the scheduler records it, and has succeeded once every year has; progress is the share of years that succeeded. Once it has succeeded, its result is today's budget-window format, built from each year's stored budget row, with totals summed in decimal arithmetic and returned as floats. A succeeded run missing a year's figures is a stored-result error. The request and response models of the budget-window routes are added; a window has 1 to 75 years and ends in 2099 or earlier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Four routes submit a budget-window report, read it with its newest run, run it again (manual, or system), and list its runs. They need the same permissions as the economy report routes, declare the same error answers for the generated client (404 and 503 as EconomyErrorResponse, 422 as EconomyRefusalResponse, and 500 on the read when a succeeded run's stored figures cannot be read back), and answer 503 without the simulation database or the Modal environment. The app builds the budget-window path from the simulation database on the economy path, and wraps the call recorder so that a reported call's end starts the budget-window years it frees, after the comparisons it lets start. create_app's wiring of the report paths moves to _report_paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After it records due calls and starts the economy work left waiting, each safety-check pass starts the next years of budget-window runs whose check is due, so a run whose caller stopped between years, or whose call report never arrived, carries on. The pass counts the years it started. The job builds the budget-window path on the same economy path, in its own Modal environment, and starts none without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Simulation Entrypoint calculates at most five years of a budget window at once, a value the contract now holds (BUDGET_WINDOW_MAX_CONCURRENT_YEARS) so the entrypoint and the Modal app share it. Each year is up to two simulation calls and one comparison, and a US national simulation runs 20 segment calls. The economy simulation functions and the comparison function rise from 10 to 40 containers, room for four windows at the cap, and the US segment function from 300 to 600, room for three US national windows. The time limits are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An operator runs authenticated_tests/test_staging_budget_window_qualification.py once against staging after the budget-window routes are deployed; no workflow runs it. It submits the same reform, region and years to the v2 route and to the old gateway's budget-window route, and requires each year's figures and the totals to agree within a relative and an absolute tolerance (0.1% and 1,000 by default). The window has more years than sim calculates at once (seven against five), so its last years start only when earlier years report their ends or the safety check passes. Running the report again unforced must read back the same figures. It needs a reform saved in the staging Public API database, given as its policy ID for the v2 report and as its parameter values for the old gateway, and both windows cost Modal time for every year, so it does not run on every staging deployment. It runs only where SIMULATION_STAGING_BUDGET_WINDOW_QUALIFICATION is 1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget-window report's result used the old gateway's models, so a change there would have changed this contract and its generated client, and the document advertised the old gateway's SPM provenance, which v2 budget windows never have. BudgetWindowReportResult, BudgetWindowYearImpact and BudgetWindowTotalImpact describe the same JSON on the v2 side, without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tten A re-run on a catalog version that lacks the report's region or dataset was recorded as a run that failed when its first year could not start, so the report's newest run became a failed one. It is refused with 422 now, as a single-year re-run is: EconomyReports.check_setup resolves both simulations' rows before the run is written. A system run returns the report's newest run before the catalog is read, as the single-year path does, so a migrated reader's first opening never fails on a version check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Releasing a budget-window run's scheduling claim cleared whatever claim it held, so a caller that finished after its lease ended could clear a newer caller's claim and let a third caller start years at the same time. claim_scheduling now returns when the claim ends, and release_scheduling releases, and sets the next check, only while the run still holds that claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scheduler took any integrity error while starting a year as another caller having started it, so a different constraint's refusal would have left that year unstarted and the run running indefinitely. It now checks that the error names the one-report-run-per-year constraint (ONE_REPORT_RUN_PER_YEAR) and raises any other, after releasing its claim. Tests cover that branch and the second pass after a year finishes while the claim is held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The economy simulation functions' 40 containers were all for four budget windows, leaving none for other reports, and the US segment function's 600 could not run the segments of the 40 US simulation calls those functions allow, let alone Stage 12's own. Now the simulation functions keep 40 (three windows at the cap and the 10 they had for other reports), the comparison function has 25 (three windows' comparisons and its 10), and the US segment function 1,000: the 20 segments of every v2 and Stage 12 US simulation call that may run at once, so no segment waits behind the limit, which would count against its caller's time limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
report_run_progress and the run error response it builds move from the economy routes to report_progress, which the economy reads, the budget-window scheduler and the budget-window reads all use, so the scheduler no longer reads statuses from a routes module and the error builder is not written twice. window_state returns the public WindowState. Revision 0005's docstring no longer says nothing writes its table, the safety-check resumption's docstring says how a failed run is tried again, and a comprehension in BudgetWindowRunStore.years no longer reuses its parameter's name. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…covery The old gateway's batch status route answers 500, with the batch's status and error, when a batch failed; the qualification raised on that answer instead of reporting the batch's error. old_gateway_status reads it, and raises any other error answer. The cap comes from the contract instead of a copy, and the module describes the operator's recovery check: promoting or rolling the entrypoint while the window runs, which it must survive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a caller whose scheduling lease had ended lost a year's report-run insert to the caller that took the lease over, the simulation runs it had created stayed pending until the safety check started them, ten minutes or more later: the other caller had reused them and so did not start them. RunStore.create_window_year_run now returns the report run already recorded for the year, and the losing caller starts the runs it created that the year compares, as a first open of a single-year report that loses does. The scheduler no longer needs to catch the conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without their window size and their years' link, a budget-window report's years would read as report runs of a single-year report, and the economy report reads would show the newest year's run as the report's. The downgrade now raises while any report has a budget window, and removes the revision's objects only when none does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tocol The budget-window path re-declared RERUN_TRIGGERS, WAITING_WORK_BATCH_SIZE, the reported-call recorder protocol and the recorded answers; it now uses the economy path's, so the two paths' refusals cannot drift apart (economy_recorders' recorded answers become the public RECORDED_STATUSES). The scheduler's docstring also says how far recovery reaches: a caller that stops between years is covered, and one that stops mid-dispatch fails its year after ten minutes, as for a single-year report. No behaviour changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eview's points The budget-window routes published the economy routes' 422 and 503 descriptions, which name economy reports; they now have their own. The window-size limit comes from the persistence library, the totals are summed field by field so the type checker sees each one, and the staging qualification also reads a failed old-gateway batch answered with 400 (a failure carrying the caller's errors) and types its polling helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch has not been 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 #981
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
After this deploys, the Simulation Entrypoint calculates budget windows on the v2 economy path itself: one reform compared with its baseline in each year of a window of up to 75 years, each year stored as an ordinary v2 economy report run, and the totals summed from the stored rows when the report is read. No user reaches it yet: the Public API's
/v2/reports(a separate PR) is its first caller. The old gateway's budget windows (/simulate/economy/budget-window) are unchanged.It is stacked on #974 (the v2 economy path), which is stacked on #951, #950, #900 and #898.
Budget-window reports and runs (persistence library, revision 0005)
simulation.reportsgainsbudget_window_size: an economy report with one compares its two simulations in each of that many years from its year (1 to 75, ending no later than the last year a run may have). Only an economy report may have one, which a check constraint andNewReportboth enforce.simulation.budget_window_runs(new) holds one row per calculation of a budget-window report on a catalog version: its trigger (initial,manual,system), whether its years calculate their simulations again (force), how many years it calculates at once (max_concurrent_years), its status (running,succeeded,failed) and error, when the safety check next looks at it, and the lease of the caller starting its next years (scheduling_claimed_until). It stores no figures.simulation.report_runsgainsbudget_window_run_id: each year of a budget-window run is one report run of the same report and catalog version (composite foreign keyfk_report_runs_budget_window_runthroughuq_budget_window_runs_id_report_catalog_version), with at most one per year (uq_report_runs_budget_window_run_id_year).RunStore.create_report_runrefuses a budget-window report's run that is not a year of its window, and a year of a window on any other report.BudgetWindowRunStorecreates runs (create_first_runadds one only while the report has none, under a transaction-level advisory lock, as first report runs are), claims and releases a run for scheduling, finishes it once with a conditional write, reads each year's report run with both its simulation runs, and reads each year'sbudget_impactsrow in year order.RunStore.create_window_year_runrecords a year's report run, or returns the one another caller recorded for that year first, so a caller that loses still starts the simulation runs it created that the year compares (as a first open of a single-year report that loses does).SELECT,INSERTandUPDATEon the new table; it needs no new grant. The new foreign key into the Public API'stax_benefit_model_versionsuses theREFERENCESgrant revision 0003 already needs.Running the years: a few at a time (entrypoint)
EconomyReportsgainsprepare_report(create a report and its simulations without running it, optionally with a window),report_setupandrun_year: one year's two simulation runs and report run, with exactly the single-year path's reuse, forcing, labour-supply and comparison rules.submitandrun_reportuse them unchanged. The single-year path neither runs nor reads a budget-window report:run_reportand the economy report reads answer 404 for one.BudgetWindowReports(budget_window_reports) submits a budget-window report and its first run, and runs one again (manual, orsystem, which returns the report's newest run when it has one, before reading the catalog). A re-run whose catalog version cannot resolve the report's region or dataset is refused with 422 before anything is written, as a single-year re-run is. A run starts its years in year order, at mostmax_concurrent_yearsat once:BUDGET_WINDOW_MAX_CONCURRENT_YEARS, five, now in the contract so the entrypoint and the Modal app share it. Its years are the batch's children; there is no Modal batch call: each year's simulation and comparison calls are the v2 economy path's own, each reporting its end as Run and store v2 economy reports in the Simulation Entrypoint #974's calls do.YearStartingRecorderwraps the call recorder, afterComparisonStartingRecorder, so a recorded comparison (or a simulation run's end) starts them. Each safety-check pass also starts the years of running runs whose check is due, so a run whose caller stopped carries on.invalid_input. Years already running finish and keep their results. Once every year has succeeded the run has succeeded.Reading and the totals
pending/queued; years a finished run never started asnot_run. A run reads as failed as soon as one year has failed, and as succeeded once every year has;progressis the share of years that succeeded.budget_impactsrow. Its models are the v2 path's own (BudgetWindowReportResult,BudgetWindowYearImpact,BudgetWindowTotalImpact), so a change to the old gateway's models never changes this contract; they leave out the old gateway's SPM provenance, which v2 budget windows never have. The totals are summed in decimal arithmetic from each float's shortest form and returned as floats, as the old gateway does (0.1 + 0.2 + 0.3totals0.6). The federal figure is the tax revenue change less the state one. A succeeded run missing a year's figures answers a declared 500.Routes
POST /v2/budget-window-reports(201),GET /v2/budget-window-reports/{report_id},POST /v2/budget-window-reports/{report_id}/runs(201) andGET /v2/budget-window-reports/{report_id}/runs(budget_window_routes), withsimulation:submitfor submissions andsimulation:readfor reads, as the economy routes. They declare the same error answers for the generated client (404 and 503 asEconomyErrorResponse, 422 asEconomyRefusalResponse, and 500 on the report read), and answer 503 without the simulation database or the Modal environment. A window has 1 to 75 years and ends in 2099 or earlier, as the old gateway's do. The generated client gainssubmit_budget_window_report,get_budget_window_report,run_budget_window_reportandlist_budget_window_report_runs.Modal capacity
In the v2 Modal app:
test_economy_run_capacitychecks these relations. Time limits are unchanged; see "Time limits".Staging qualification against the old gateway: an operator step
authenticated_tests/test_staging_budget_window_qualification.pyis an operator step, not a CI check. An operator runs it once against staging after this is deployed there. It submits the same reform, region and years to the v2 route and to the old gateway's route, and requires each year's figures and the totals to agree within 0.1% or 1,000 (both configurable); its window has seven years against the cap of five, so its last years start only when earlier years report their ends or the safety check passes; and running the report again unforced must read back the same figures. It is an operator step because the v2 report needs a reform saved in the staging Public API database (given as its policy ID, and as its parameter values for the old gateway), which nothing creates in CI without writing to staging, and because both windows cost Modal time for every year, too much for every staging deployment. The module's docstring lists every input.Decisions, for the reviewer
Overlap
runs.py,run_store.py,app.py,safety_check.py,simulation_database.pyand the v2 Modal app. This PR keeps its own code in new modules (budget_window_runs,budget_window_store,budget_window_reports,budget_window_reads,budget_window_routes,budget_window_schemas) and changes those shared modules only where it must; whichever merges second rebases.economy_routesmakes three helpers public for the budget-window modules (report_run_progress,simulation_response,answer_route), with no change in behaviour.Time limits
Unchanged, as decided: the US economy simulation call 3,900 s, the UK one 3,000 s, each segment call 3,600 s, the comparison 1,800 s. A US national simulation call's limit includes waiting for its segment calls, and is only 300 s longer than a segment's own, so segment calls queued behind a container limit count against it. The new segment limit covers every US simulation call the functions allow at once; whether the Modal workspace can run that many 8-CPU, 32 GiB containers at once is not known here, and if it cannot, segments queue and a slow year could time out, which fails its window.
Environment variables
No new variable. The changed code reads:
SIMULATION_DATABASE_URL(entrypoint and safety-check job): unchanged wiring from Give the Simulation Entrypoint its own simulation schema, migration chain and database connection #898 and Record simulation runs and report runs, and their outcomes when Modal reports a call ended #950: Secret Manager, named by thebeta/prodvariableSIMULATION_DATABASE_URL_SECRET_NAME, mounted bydeploy-cloud-run-simulation-entry.shand copied into the safety-check job. The budget-window path uses it as the economy path does. Revision 0005 needs no new grant. Not verified in a deployment.MODAL_TOKEN_ID,MODAL_TOKEN_SECRET(entrypoint and safety-check job; they start each year's calls): unchanged from Record simulation runs and report runs, and their outcomes when Modal reports a call ended #950 and Run and store v2 economy reports in the Simulation Entrypoint #974: Secret Manager, named byMODAL_TOKEN_ID_SECRET_NAMEandMODAL_TOKEN_SECRET_SECRET_NAME. Not verified in a deployment.SIMULATION_CALL_REPORT_MODAL_ENVIRONMENTandAPP_ENVIRONMENT: unchanged from Run and store v2 economy reports in the Simulation Entrypoint #974; the budget-window path is built only where the economy path is, in the same Modal environment and artifact environment.max_containerschanges; their secrets and variables are Run and store v2 economy reports in the Simulation Entrypoint #974's.SIMULATION_ENTRYPOINT_TEST_BASE_URL,SIMULATION_ENTRYPOINT_TEST_AUTH_ISSUER,_AUDIENCE,_CLIENT_ID,_CLIENT_SECRET,SIMULATION_STAGING_BUDGET_WINDOW_QUALIFICATIONand theSIMULATION_QUALIFICATION_*inputs come from the operator's shell; the auth values are thebetaenvironment's test-client secrets the deployed-auth checks use. No workflow passes them.Waits for
MODAL_WORKSPACE_IDand the safety-check schedule./v2/reportsbudget-window support (a separate PR) needs a published release of the simulation client containing these routes; publishing follows the production deployment.Testing
Locally, each commit was written test-first: each new or changed test was seen failing for the expected reason, then passing. Each commit also ran ruff 0.15.22 (format and check), Pyright on each changed package (
python -m scripts.rules pyright), andscripts/run_quality_guards.py; every commit in the branch was checked with ruff and the guards again in a clean worktree.Tests run (only the tests this branch adds or changes; CI runs the rest):
tests/test_budget_window_runs.py(new: the revision's upgrade,alembic check, downgrade to 0004 and upgrade again, the refused downgrade, the store's claims and releases, simultaneous claims, first runs and year inserts) and the changed test intests/test_schema.py;tests/test_economy_report_years.py,test_budget_window_reports.py,test_budget_window_reads.py,test_budget_window_routes.py,test_budget_window_safety_check.py,test_budget_window_qualification.py(all new), and the new or changed tests intests/test_openapi.py;tests/test_economy_run_capacity.py(new) and the changed test intests/test_stage12_modal_app.py.The OpenAPI document was generated from this branch and the Python client from it with
scripts/generate-clients.sh. Driven against the app with in-memory stores, the client parses each new route's answers: 201 and 200 bodies, 404, 422 and 503.Not run: the whole test suites, and anything against a deployed service, Modal app or database, including the qualification.
🤖 Generated with Claude Code