Skip to content

Calculate budget windows on the detailed-save path, five years at a time - #1054

Merged
anth-volk merged 51 commits into
mainfrom
v2plan/c1-13-budget-windows
Nov 12, 2026
Merged

anth-volk merged 51 commits into
mainfrom
v2plan/c1-13-budget-windows

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1053

Same-Repository Draft PR Check

  • This PR is a draft.
  • This PR is from a branch in PolicyEngine/policyengine-sim-api, not a personal fork.

Changelog

  • This PR adds changelog fragments: changelog.d/v2-budget-windows.added.md and changelog.d/v2-budget-window-capacity.changed.md.

Summary

Budget-window reports on the detailed-save path. A budget-window report compares a reform with its baseline in each year of a window of 1 to 75 years. Its calculation is one budget_window_runs row with one report run per year; at most five years are under way at once. Each year's coordinator, when its year ends, starts the window's earliest waiting years, and the window is marked succeeded once every year has succeeded, or failed as soon as a year fails. This replaces the draft #982, which was built on the simulation service's own tables and scheduled its years through call reports, both since removed.

Simulation service: POST /reports with report_type: "budget_window"

  • Request. The body names budget_window_size (1 to 75, ending by 2200) and otherwise the same fields as an economy report, except include_cliffs, which is refused (422). POST /reports tells the two apart by report_type (a discriminated body). A refused body's 422 locations are exactly what an economy report's were before (ReportRequestRoute removes the type Pydantic adds), because the Public API passes them to its callers.
  • Rows. In one transaction: an economy report with budget_window_size set and year the first year; inputs holds {"include_cliffs": false} plus spm when given, the same as a single-year report without cliffs, so reuse (which compares inputs) works both ways between windows and single-year reports. One budget_window_runs row (pending, trigger initial, recalculate false, the default worker's catalog version). For each year, a report run naming the window run and the year, on the same versions. Each year has its own baseline and reform simulations of that year, unless reuse finds succeeded runs of that year (find_reusable_run per year; the two sides never share a run). The report names the first year's simulations.
  • Start. The service then advances the window as its own role (BudgetWindowStore.advance_window), which starts the earliest five years under the window run's row lock. The first year's coordinator input is built as a single-year report's (report_run_calculation); each later year's is derived from it (year_calculation): only the year, its three runs, its artifact prefix and its call's creation time differ. The first years share one ten-second wait for Modal, counted from the first start: a later year not confirmed within it is deferred (YearStartDeferred) and waits for a started year's coordinator.
  • Answers. 201 with report_id, report_run_id (the first year's run), budget_window_run_id, status and every year's report run and simulation runs. A year Modal refuses, or a first year it doesn't confirm in time, fails the window and its waiting years, and answers 503 modal_call_refused naming report_id. Otherwise the economy report's error rules apply (400 and 422 before any write, 500 internal_error, no exception text in detail). The Public API's adapter reads report_id and budget_window_run_id and ignores the rest (agreed with tracker task C1 · 14).
  • Ready check. /ready now requires the service role's UPDATE on budget_window_runs' status, error_message, completed_at and updated_at, which Let the simulation roles update a budget-window run's status policyengine-api#4267 grants.
  • GET /reports/{report_id} on a window answers one of its years' runs; the window's own status is in budget_window_runs, which the Public API reads.

Stage 12 side (Modal coordinator, workers and sweep)

  • BudgetWindowStore (Stage 12 persistence library): an advance locks the window run's row, then fails the window and its waiting years (and their unstarted simulation runs) when a year failed, marks it succeeded when every year succeeded, or starts the earliest waiting years until five are under way, recording each call while the year is still pending with no call. A start Modal refuses fails that year and the window, unless the year's own call claimed it meanwhile. The Modal worker role and the service role may both advance a window.
  • Each year's coordinator. The v2 coordinate_report runs every call through coordinate_window_year: it looks the year's window up once, marks the window running before the coordinator runs, and advances it after the coordinator returns, fails or is cancelled, starting the next years in the same Modal application (ModalYearStarter). A pre-emption advances nothing; the rerun advances when it ends, even when it finds its year already finished. A report run outside any window makes no window writes, and no window write changes a year's own outcome.
  • Stuck-run sweep. It leaves a waiting year of an unfinished window to the window, counts a year started later from when its call was recorded, fails the window when it fails one of its years, and settles a window that nothing has changed for ten minutes with no year under way: from its years when all have ended, or as stalled (budget_window_stalled).
  • Container limits. The coordinator may run 25 calls at once (three windows at five years, and ten other reports), each simulation worker 50 (both simulations of every coordinator call), and the US segment function 1,000 (20 region groups per US simulation call).
  • Contract. year_calculation and report_run_artifact_prefix in stage12_execution, shared by the service and the coordinator; the artifact environment check lives there once.

From #982

Ported: the cap of five, the year order, "the first failed year fails the window", the container-limit test. Dropped: the simulation service's own tables and revision 0005, call reports and their safety-check scheduling, totals summed on read (the Public API's, C1 · 14), and /v2/budget-window-reports.

Builds on, and what merges first

Environment variables and secrets

No new variable is read.

Name Provider Wiring Verified
SIMULATION_SERVICE_DATABASE_URL Secret Manager, named by the GitHub Actions variable SIMULATION_SERVICE_DATABASE_URL_SECRET_NAME unchanged: the Cloud Run deploy script mounts it (add_runtime_secret); the window store uses the service's existing engine Wiring traced. Unverified: the role holds #4267's grants in staging and production (not deployed yet).
MODAL_WORKER_DATABASE_URL Modal secret policyengine-modal-worker-database attached to coordinate_report (report_run_writer_secrets) and to the sweep's sweep_stuck_runs (secrets=[...]), both with required_keys The secret exists in staging and main (B1 · 7). Unverified: the role holds #4267's grants.
MODAL_TOKEN_ID, MODAL_TOKEN_SECRET, SIMULATION_V2_MODAL_ENVIRONMENT, SIMULATION_V2_MANIFEST_NAME, V2_SIMULATION_OUTPUT_BUCKET unchanged from #1052 unchanged As in #1052.

A coordinator starts the next year's call with modal.Function.from_name(...).spawn, using the credentials Modal's container provides, as it starts its workers.

Testing

  • Every test this branch adds or changes was run individually by node ID: the budget-window store (28, against PostgreSQL 17 built from Let the simulation roles update a budget-window run's status policyengine-api#4267's head, as the Modal worker role and the service role), the window records (7, same database), the contract derivation (9), the coordinator scheduling and wrapper (19), the year-start binding to the real coordinator (2), the sweep (6, plus the app test), the capacity limits (3, plus the Modal app test), the service's request, submission and route tests (10, 9, 14), the OpenAPI tests changed (3), the access test (1) and the regenerated client's window tests (3). Failing tests were committed first, with "(failing)" in the subject.
  • Sim's checks (Ruff, rules rendering, quality guards, lock checks, Pyright) pass for every changed package.
  • The Postgres-backed store tests (REPORT_RUN_TEST_DATABASE_URL) skip in CI, as Coordinate economy runs through the run tables instead of the evaluation records #1051's report-run tests do: no workflow sets that variable.
  • Five read-only review rounds (review-code): rounds 1 to 3 and two short rounds on later fixes. Every substantiated bug was fixed test-first; the reports are in the session's notes.

🤖 Generated with Claude Code

anth-volk and others added 30 commits November 12, 2026 19:13
A budget window's years are report runs naming its budget-window run. The
tests cover the store that starts the earliest waiting years while at most
five are under way, fails the window and its waiting years when a year
fails or Modal refuses a year's call, marks the window succeeded when its
last year succeeds, marks it running when a year is claimed, and settles a
window that no year is advancing. They also cover the stuck-run sweep's
listing, which leaves a window's waiting years to the window. They fail
because the store does not exist yet.

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

The Stage 12 persistence library gains a budget-window store. An advance
locks the budget-window run's row, fails the window and its waiting years
(and their unstarted simulation runs) when a year failed, marks it
succeeded once every year succeeded, and otherwise starts the earliest
waiting years until five are under way, recording each call while the year
is still pending with no call. A start Modal refuses fails that year and
the window. A claimed year marks its window running, and a window that no
year is advancing can be settled: finished if its years have ended, failed
as stalled if years are still waiting. The stuck-run sweep's listing leaves
a year waiting in an unfinished window to the window, and names the window
of each report run it lists.

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

When the stuck-run sweep fails a year of a budget window, it should fail
the window too, starting nothing, and after judging the runs it should
settle each window that no year has advanced for the minimum age. The
scheduled function should pass the budget-window store to the sweep and
return the windows it finished. They fail because the sweep takes no
window store yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the sweep fails a year of a budget window, it fails the window too,
with the window's waiting years, and starts nothing. After judging the
runs, it settles each unfinished window that nothing has changed for the
minimum age with no year under way: from its years when they have all
ended, or as stalled when years are still waiting. The scheduled function
gives the sweep the budget-window store, built from the same Modal worker
database secret, and returns the windows it failed or marked succeeded.

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

A year's coordinator should mark its window running when it claims the
year, and when the year ends advance the window, starting the next years'
coordinators in its own Modal application by report run ID. Neither step
may change the year's own outcome: a failure is logged without its text,
and only a pre-emption or cancellation propagates. The store should name
the window a report run is a year of. They fail because neither exists
yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A coordinator marks its year's window running when it claims the year and,
once the year has ended, advances the window: it starts the earliest
waiting years in its own Modal application's coordinate_report by report
run ID, or finishes the window. Each step logs a failure by its type only
and never changes the year's outcome; a pre-emption or cancellation
propagates. The store names the window a report run is a year of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator should hold three budget windows at five years each beside
the ten calls it had for other reports, each simulation worker both
simulations of every coordinator call, and the US segment function every
segment of every US simulation call, so no call waits behind a container
limit while its caller's wait runs. They fail on today's limits of 10, 10
and 300.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The coordinator may run 25 calls at once: three budget windows at five
years each, and the ten it had for other reports. Each simulation worker
may run 50, both simulations of every coordinator call, and the US segment
function 1,000, one per region group of every US simulation call, so no
call waits behind a limit while its caller's wait runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rst (failing)

A year differs from the window's first year only in its year, its three
runs, its artifact prefix and when its call was made, so whoever starts a
year derives its coordinator input from the first year's. The store should
give its starter each year with its runs, and a coordinator's year starter
should spawn its own application's coordinator with the derived input.
They fail because the derivation and the year passed to the starter do not
exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract library derives a year's coordinator input and context from
another year's: only the year, the three runs, the artifact prefix and the
call's creation time change, and the retention period keeps its length.
It also builds a run's artifact prefix, so the simulation service and the
coordinator name it alike. The budget-window store gives its starter each
waiting year with its runs, and a coordinator's year starter spawns its
own application's coordinate_report with the derived input.

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

The simulation service fails a budget-window run when Modal refuses one of
the first years it starts, so its ready check should require UPDATE on the
window run's status, error message, completion and update times, as the
Public API's privilege manifest is to grant. It fails because the check
lists no budget-window columns yet.

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

The simulation service fails a budget-window run when Modal refuses one of
the first years it starts, so its ready check now requires UPDATE on the
window run's status, error message, completion and update times. The
Public API grants them in PolicyEngine/policyengine-api#4267, which must
deploy before this does, or the service's ready check fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A year an earlier year's coordinator started late should count from when
its call was recorded, not from its creation at submit, so the sweep's
maximum age doesn't fail a year queued behind Modal's container limit. A
start Modal refuses should log its cause's type and give that year's own
simulation runs the refusal. The artifact prefix should refuse an
environment that could leave it and a creation time without a time zone,
as the executor's prefix does. A new test confirms the simulation
service's role can start and fail a window with the same store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A year an earlier year's coordinator started counts, until its claim, from
its last change, when its call was recorded, so the sweep's maximum age no
longer fails a year that waits in Modal's queue a day after the service
created it. A start Modal refuses is logged by the error's type, and that
year's simulation runs record the refusal instead of another year's
failure. The contract's artifact prefix refuses an environment that could
leave it and a creation time without a time zone, and the executor's
comparison prefix now uses it. The store's description says the window lock
is held across the Modal calls that start years.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 coordinator function should mark its year's window running before
the coordinator runs and advance the window after the coordinator returns,
fails or is cancelled, but not after a pre-emption, which Modal reruns. A
call naming no report run touches no window, and a window store that
cannot be built leaves the year to run. The year starter should take the
coordinator's raw input and check it only when it starts a year. They fail
because the wrapper does not exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 coordinator function marks its year's window running before the
coordinator runs and advances the window once the coordinator returns,
fails or is cancelled, starting the window's next years in its own
application. A pre-emption advances nothing: Modal reruns the call, which
advances when it ends, even when it finds its year already finished. A
call naming no report run touches no window, and a window store that can't
be built is logged by type and leaves the year to run. The year starter
takes the coordinator's raw input and checks it only when it starts a year.
This also rewraps the sweep's description, which review round 1's fix left
with an overlong line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges v2plan/c1-8-run-table-coordination (#1051):
the coordinator takes a report run's input and context with no evaluation
record, and each worker claims its own simulation run. The Stage 12
persistence README and the Modal worker database access module keep both
branches' descriptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 found that the year starter's call didn't fit main's
coordinator, which required an evaluation record. Merging C1 · 8 made the
record optional for report runs, which fixed it; these tests keep it fixed.
The started call binds to the v2 coordinator function's signature with no
record, and the real coordinator claims the started year's report run under
the started call and calculates it. They couldn't be written failing first:
the run-table test helpers they use arrived with the merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merges v2plan/c1-9-economy-simulations-reports (#1052),
which already contains C1 · 8's #1051: the simulation service's
POST /simulations and POST /reports, their records and reads, and the
economy staging check. The merge had no conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget-window report reads its catalog rows and both sides' values once,
and each year's reusable runs, and is created with one budget-window run
and a report run per year. Each year has its own simulations, of that
year, so reuse works between a window's years and single-year reports. A
window must name consecutive years from its first, at most 75. They fail
because the window records don't exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation persistence library creates a budget-window report as an
economy report with its window's length, one pending budget-window run,
and for each year a report run naming it and the year, on the same
versions. Each year's sides that reuse no run get a new simulation and
run of that year, so a window's years and single-year reports reuse each
other's runs. The store reads the catalog rows and both sides' values once,
and each year's reusable runs. A window's years must be 1 to 75
consecutive years from its first, ending by 2200. The economy records'
helpers that insert a simulation with its run and turn a refused key into
EconomyRecordReferenceError are now public for it, and a refused
budget-window run key is named too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window failed as stalled should record the stall on its waiting years
and their simulation runs, not another year's failure. A refused start
should name its cause's type in the year's message. The coordinator's
wrapper should look the year's window up once, make no window writes for
a report run outside any window, look it up again when that first lookup
failed, and still advance the window when the call's observability
context can't be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window failed as stalled records the stall on its waiting years and
their simulation runs, not another year's failure, and a refused start
names its cause's type in the year's message. The coordinator's wrapper
looks the year's window up once: a report run outside any window makes no
window writes, a failed lookup is retried once the year ends, and a call
whose observability context can't be read still advances its window. The
artifact environment check lives once, in the contract. The store and
scheduling descriptions say the window is marked running before the
coordinator claims its year, and the Stage 12 migration document describes
budget windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service waits a limited time for Modal to accept a call,
and Modal may accept it later, its coordinator claiming the year before
the failed start is recorded. That year is under way, so the window should
go on starting its other years rather than fail. It fails because any
failed start fails the window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a start raises but the year is no longer waiting, because Modal
accepted its call after the starter stopped waiting and the coordinator
claimed it, the year counts as started and the advance goes on with the
next years. Only a year still waiting is failed as refused, with its
window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A budget-window request names its first year and its window's length (1
to 75 years, ending by 2200), sends no cliff option, and stores the options
a single-year report without cliffs stores. The service reads the window's
inputs once, creates a report run per year, then advances the window,
which starts its first five years; the first year's coordinator input is
built as a single-year report's, and each later year's is derived from it.
A year reuses its own year's run. A first year Modal refuses or doesn't
confirm in time answers 503 naming the report, and an advance that can't be
written is answered as created. The route sends a window to the window
service and an economy report to the report service. They fail because the
window request, service and route don't exist yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
POST /reports should take an economy report or a budget window, told
apart by report_type, and answer 201 with either's created record. The
budget-window request should name its length (1 to 75) and no cliff
option, and its schemas should be the Cloud Run entrypoint's own. They
fail because the route takes only economy reports.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 7 commits November 12, 2026 21:53
POST /reports runs through a route class that relocates a refused body's
errors as an economy report's were: the matched request type leaves each
field's location, and a missing or unknown report_type is refused at
report_type. The first years a request starts share one ten-second wait
for Modal; a later year not confirmed within it is deferred
(YearStartDeferred), which leaves it and the later years waiting for a
started year's coordinator instead of failing the window. The window
service's logs use the economy service's record fields, the window
request shares the economy request's helpers and the last-year bound, an
unused property is gone, and the route description, the changelog
fragment and the README describe budget windows, including what
GET /reports answers for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unknown report_type should be refused with the value sent, whatever its
JSON type, and a body with none as an economy report's was. The first
years' shared wait for Modal should start when the first year starts, not
before the advance's lock and reads. Two guards: a later year Modal refuses
outright still fails the window, and no later year's call starts once the
wait has run out. The value test and the wait test fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first years' shared wait for Modal starts with the first start, after
the advance's lock and reads, so database time no longer comes out of the
first year's wait. An unknown report_type is refused with the value sent,
as JSON, not its text. The report types and the expected-types message are
derived from the request models, and the window records use the tables'
bounds instead of copies. A deferred later year is logged. The README, the
window service's and store's descriptions, the route description and the
Stage 12 migration document describe the shared wait and deferral, and
that a body with no report_type is refused for that alone.

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

When the first year's call uses the whole shared wait, the next year is
deferred without its call being started, and the deferral's log names the
year's report run. It fails because the log names only the year.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deferred year's log names its report run as well as its year. The
report route's body type is one shared alias, from which the error
relocation reads the report types. The README and the window service's
description say up to the first five years start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service's economy calls now name their artifact prefix
with report_run_artifact_prefix, as each budget-window year derived from
them does, so both build it alike and refuse an environment that could
leave the prefix or a creation time without a time zone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 2 commits November 13, 2026 00:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…reports' into v2plan/c1-13-budget-windows

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Base automatically changed from v2plan/c1-9-economy-simulations-reports to main November 12, 2026 20:23
anth-volk and others added 4 commits November 13, 2026 01:17
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Public API's #4267 grants the simulation service's role UPDATE on
budget_window_runs' status, error_message, completed_at and updated_at,
which the service's readiness check and its window advances need. Its
merge commit is the head of the Public API's master. Its v2 migrations
and privilege manifest are the same as at the #4267 head the budget-window
store and records tests ran against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The simulation service's role now needs UPDATE on budget_window_runs'
status, error_message, completed_at and updated_at, so the fixture of
every needed privilege lists them, and the fixture missing two others
still reports only those two.

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

The v2 coordinate_report hands the runtime's capture_context to
coordinate_window_year, which uses it to start a budget window's next
years. The real runtime has it; the test's recording runtime didn't, so
coordinate_report's cases failed before the coordinator ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 4 commits November 13, 2026 01:37
The window wrapper starts a budget window's next years with the
observability context the coordinator's runtime captures when they are
started. The test now gives the coordinator a runtime with its own
capture and checks that the wrapper receives that capture, uncalled.
Seen failing with the capture replaced by one that returns nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The budget-window store marks a window running, advances it and settles
it as the Modal worker role, through report_run_tables' mapping.

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

The real runtime captures the trace of the operation that is open when
the capture runs, so the next years' calls carry the coordinator's trace
only when the window captures it inside the coordinator's operation. The
test's runtime now records whether that operation is open at the capture,
and the wrapper's stand-in captures after the year's coordinator returns,
as the wrapper does. Seen failing with the capture taken before the
operation opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anth-volk
anth-volk marked this pull request as ready for review November 12, 2026 22:50
@anth-volk
anth-volk requested a review from a team as a code owner November 12, 2026 22:50
@anth-volk
anth-volk merged commit 7655585 into main Nov 12, 2026
28 checks passed
@anth-volk
anth-volk deleted the v2plan/c1-13-budget-windows branch November 12, 2026 22:50
anth-volk added a commit that referenced this pull request Nov 12, 2026
Brings in the budget windows (#1054). Both sides' imports are kept; the
call-refused answer is public under main's name with the re-run keyword;
the report route dispatches budget windows as main does, with the public
answer helpers. The budget-window service is generic over its store, and
the re-run service extends it, so the app builds one service that creates,
re-runs and starts budget windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 12, 2026
…indows

#1054 (budget windows) merged into main, and also made the POST /reports
body a union. The merge keeps both report types:
- one ReportRequest of all three types (economy, budget_window, household)
  in a new report_api_models module, replacing the two-type unions each
  branch had, and report_request_errors importing it;
- create_report dispatching household reports to _household_report_answer
  and the other two to a new _economy_report_answer, which keeps the
  router function under Ruff's complexity limit;
- response_model listing the three answers, and the route's summary and
  description naming all three;
- both packages' exports and the Stage 12 persistence README keeping
  both sides.
#1054's tests that used "household" as an unknown report type now use
"not_a_report_type", and the unknown-type message lists the three types.
The OpenAPI tests check the three-type discriminator and 201 answers, and
the household request's fields in a test of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Nov 12, 2026
Brings in #1054's budget windows. The sweep's 15-minute age now also
times its settling of idle windows, and the migration document and the
sweep app's docstring say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Calculate budget windows on the detailed-save path, five years at a time

1 participant