Skip to content

Segment and cache Stage 12 US simulations - #712

Merged
anth-volk merged 32 commits into
mainfrom
feat/stage12-segmentation-cache
Oct 19, 2026
Merged

anth-volk merged 32 commits into
mainfrom
feat/stage12-segmentation-cache

Conversation

@anth-volk

@anth-volk anth-volk commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #711
Fixes #792

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.

Summary

  • Split each Stage 12 US baseline and reform simulation into 20 deterministic segment calls, while keeping the implementation independent of API v1 runner modules.
  • Add versioned dataset and baseline cache construction, storage, image loading, manifest identity, and deployment validation.
  • Observe baseline and reform concurrently within one shared wait window. Cancel every unfinished simulation, and record it as failed, when one simulation fails, when the window closes, or when the coordinator itself fails after starting a simulation.
  • Preserve structured policy-validation errors, including invalid effective-period details, through simulation and report records.
  • Document Stage 12 cache deployment, segmented execution, aggregation, and failure handling.
  • Rebased onto main after Release Stage 12 database sessions and keep original failures visible #789. Every Stage 12 database operation in this PR opens its connection through Release Stage 12 database sessions and keep original failures visible #789's connection opener, and every report and worker failure record goes through Release Stage 12 database sessions and keep original failures visible #789's persist_failure_record.
  • Review fixes: an error reading or writing a failed simulation's own record no longer replaces its failure, each failure is reported to telemetry exactly once, and the Stage 12 precompute app mounts only the read-only pe-uk-private-hf-read-token Hugging Face secret. See "Fixes after review" below.
  • Second review fixes: baseline cache files now have the names .py saves and loads, the v2 manifest stays readable by readers released before the cache, the immutable cache has no forced rebuild, and a failed determinism check cannot be skipped by running again. See "Fixes from the second review" below.
  • Merged main after Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) #799 and the .py 6.7.5 update: twelve conflicts are resolved, and main's ruff rule groups 2 to 7 are applied to this PR's code. See "Merged main after Enable ruff structure and import rules (INP, PLC, SLF, N, A, FAST, E) #799 and the .py 6.7.5 update" below.

Merged main after #799 and the .py 6.7.5 update (28759410)

Main is merged in as a merge commit, as before. Main brought .py 6.7.5 (#818, #820, #822, #826), ruff rule groups 2 to 7 (#773, #777, #779, #783, #785, #799), the Auth0 permission checks on the entrypoint routes (#758), job stage reporting (#781) and the policyengine-observability 3.0.2 lockfiles (#816).

Conflicts

Twelve files conflicted.

  • Coordination. This PR's cancellation handling, shared wait window and failure reporting are kept. Main's _require_artifact_digest helper is kept; its merged call sites already use it. Main's _mark_child_failed is dropped, because this PR had already replaced it with _record_child_failure and the cancellation records. The wait-window constant still comes from stage12_timeouts.
  • Aggregation. This PR's PrecomputedSimulation stays in the aggregation module, and main's module-level build_spm_comparison_provenance import is kept.
  • Everywhere else. The conflicts were main's docstrings, prebuilt exception messages and TYPE_CHECKING imports against this PR's cache, segmentation and cancellation additions. Each kept both: this PR's behaviour, in main's style.

Ruff rules from main (28759410, 2a81eba1)

Main's new rule groups found 372 findings in this PR's code. All are fixed. The only suppressions this PR adds are nine catch-all handlers (BLE001), each with its reason: they record a redacted cause and raise a fixed error from None, or log a failed best-effort write or cancellation without replacing the original failure. Ruff accepts a catch-all only if it re-raises, chains or logs the original error with its message, which these handlers deliberately avoid. Main's coordinator suppresses its own catch-all for the same reason.

  • Deferred imports are module-level imports. This covers the precompute app, the image-build download, the cache build, worker validation, qualification, segment fan-out and the v2 app. Where a test patches a module, the code imports the module and calls through it.
  • Two import cycles are broken by moving code. Stage12CachedSimulation, qualifying_cache_identity and the cache outcomes move from stage12_cache.runtime to stage12_runtime.cached_simulation. should_segment_simulation moves into stage12_runtime.partition. The simulation module now imports both at module level, and the stage12_cache package no longer imports .py.
  • An incomplete stored baseline clears .py's in-memory cache. .py's Simulation.ensure keeps a process-wide in-memory cache of up to 100 simulations. When a stored baseline fails the output plan, Stage12CachedSimulation.ensure recalculates and saves it. It then calls the cache's public clear(), instead of replacing one private entry, so the incomplete output cannot be served from memory; the next ensure loads the saved file. A new test covers this.
  • Types. The precompute's Modal handles are annotated modal.Function, which replaces this PR's copy of the RemoteFunction protocol; Stage 12 code must not import the v1 precompute_models. The fan-out's Modal module parameter is a ModuleType. The other Any parameters are typed where a precise type exists.
  • Raises. Two raises inside try blocks move into helpers. The image build's two manifest type checks raise TypeError.
  • Names. Stage12Cancellation is renamed Stage12CancellationError (N818); it is internal to this PR.
  • Logging. A segment cancellation that raises is logged with its error type instead of being ignored silently.

.py 6.7.5

The lockfiles are main's; this PR doesn't change them. The test that runs the real bundle and US model, and checks that the planned baseline file is the one .py saves and loads, passes on 6.7.5. Between 6.4.1 and 6.7.5, Simulation.ensure only gained reruns of saved outputs that .py marks as outdated. The in-memory cache copy has existed since 6.0.0.

Environment variables

This merge adds or removes no environment variable, workflow input, secret or Modal secret attachment. The conflict resolutions and ruff fixes touch only Python modules and tests. The deployed v2 application name follows the .py version, as on main.

Validation

On .py 6.7.5, merged with main at 35857b2e:

  • Executor suite: 1,292 passed, 22 skipped, 3 deselected. This includes main's tests that every runtime-evaluated Pydantic model resolves its annotations.
  • Simulation entry: 534 passed. Contract: 195 passed. Stage 12 persistence: 19 passed. Observability: 32 passed.
  • ruff check . and ruff format --check projects/*/src libs/*/src with the pinned ruff 0.15.22 pass.
  • python -m scripts.rules guard --base-ref origin/main and scripts/run_quality_guards.py pass. Shellcheck passes on the changed scripts.
  • Not run: new testing, staging or production deployments.

Fixes from the second review (b68e5545, ea82c92f)

Main is merged in first (d007697a, after #765), with ruff's group 1 rules applied to this PR's files.

Baseline cache files now have the names .py uses

  • Problem. .py saves and loads a simulation's output as Simulation.storage_id + ".h5". A US simulation always resolves an SPM selection, so storage_id is <id>-spm-<sha256 of the selection>. The cache named baselines <simulation_id>.h5. compute_baseline_impl therefore never found the file it had just calculated ("Stage 12 baseline calculation produced no cache file"), which would fail every Stage 12 deploy, and no worker could have loaded a baked baseline.
  • Fix. The baseline identity now holds the resolved SPM selection the worker builds the simulation with: normalize_runtime_spm on the same parameters, which are now built by one shared simulation_runtime_params. Its filename mirrors storage_id. The selection is part of the identity digest, so an explicit selection equal to the defaults shares the file, and a different selection gets its own. Plan entries carry the file name, and the manifest, the determinism check and the image use it.
  • Test. test_planned_baseline_file_is_the_one_the_worker_saves_and_loads plans with the real bundle, builds the worker's model with the real US model through build_stage12_simulation, and compares the name from .py's own output_dataset_filepath with the planned name. It fails with the old name.

The v2 manifest stays readable by readers released before the cache

  • Problem. V2WorkerVersion and V2WorkerValidation gained cache, and the manifest models forbid unknown fields. Readers already deployed would reject the published manifest, and a cold reader would then have no valid manifest. The manifest is published before the reading revision is promoted, staging deploys do not promote, and a rollback or revert would read the same document.
  • Fix. publish_v2_manifest writes the manifest under a new active-with-cache key, and the same manifest without the cache fields under active. The loader and the publisher read active-with-cache, falling back to active only before the first publish that writes both keys. Removing the active copy needs a later change, once no deployed or rollback revision reads it.
  • Checked against main's models. Main's loader reads the active copy and rejects active-with-cache, and main's publisher (as after a revert) still publishes on top of it. A contract test pins the exact fields earlier readers accept.

No forced rebuild of the immutable cache

  • Problem. The v1 precompute's force_recompute input, which main deploys pass through, also reached the Stage 12 cache step. Forced uploads hit create-only paths, and HDF5 baselines are not byte-reproducible. The store therefore raised "immutable Stage 12 cache path contains other data", failing staging and skipping production.
  • Fix. The Stage 12 cache has no force option. The workflow passes only the Modal environment, the script and the app take no --force, and select_work selects only missing files. The v1 precompute still receives force_recompute. To replace a bad file, delete it and deploy again: the run recomputes it and repeats the determinism check.

A failed determinism check cannot be skipped by running again

  • Problem. Baselines are uploaded before the check. After a failed check, the next run found every baseline present, skipped the check and published them.
  • Fix. A pass is recorded at determinism/us/<digest>.json, keyed by the planned baseline identities. A run checks when it computed baselines or when no pass is recorded, and publish_manifest_impl refuses to publish without the record. The check now also requires the stored baseline to load as a cache hit, so it cannot compare two fresh calculations.

Not changed

Policy effective periods stay strict ISO 8601: a date such as 2027-4-6 is rejected with invalid_policy_period. This is intended.

Environment variables

No variable is added or removed.

  • The cache step still reads MODAL_TOKEN_ID and MODAL_TOKEN_SECRET (GitHub Actions secrets) and STAGE12_CACHE_BUCKET (GitHub Actions variable, in the step's env). The script writes to the runner's GITHUB_OUTPUT. Only the positional force argument is gone.
  • The determinism record is written by the precompute functions with the same stage12-evaluation-gcp-credentials Modal secret that uploads the baselines, in the same bucket under a new determinism/ prefix.
  • The new manifest key is written by the existing publisher step with the same Modal token.
  • Not verified in this update: the cache buckets' IAM grants for the precompute service account. That is unchanged from before, but the new prefix relies on the same grants.

Validation

Merged with main at 9cd0ca3c, on .py 6.4.1:

  • executor: 1,238 passed, 22 skipped;
  • entry: 449 passed;
  • persistence: 19 passed;
  • contract: 156 passed.

ruff 0.15.22's lint and format checks, shellcheck on the changed script, and the rules-registry and ci-scripts guards also pass.

Fixes after review

A failed simulation's own state error no longer replaces its failure (b6c691d1)

Before this fix, the coordinator read a failed child's record, to choose its failure detail, outside any guard. A reproduction in which one database read of the failed baseline's record fails recorded the report as report_coordination_failed instead of simulation_invocation_failed, recorded the failed baseline itself as simulation_cancelled ("Cancelled after the coordinator failed."), and gave the reform the same wrong reason. A failing write of that record was already caught, but it was logged without the report or simulation identifiers.

  • In both the child-wait path and the dispatch-failure path, reading and writing the failed simulation's own record is now non-fatal, like the cancellation writes. The error is logged at ERROR with evaluation_id, simulation_execution_id, the role and the error type, in the message and as record attributes. The error's message is not logged.
  • The report keeps the simulation's failure as its code and cause (simulation_invocation_failed or simulation_dispatch_failed, or the typed failure the worker recorded).
  • The failed simulation is recorded as failed with its own cause whenever its record can still be written. If the first read fails, the conditional write is still attempted. It is never recorded as simulation_cancelled, and it is not cancelled through Modal.
  • Its peers are cancelled with "Cancelled after the simulation failed.".

Each failure is reported once

#789's contract is that each failure is reported once. After the rebase, an unexpected error reached telemetry twice: once through #789's report_original_failure ("Stage 12 execution failed", with the error), and again through this PR's failure_detail_from_exception, which called log_and_redact_exception. That second report was a standard-library ERROR log with the stack trace, runtime.record_exception and a gateway_error_redacted event. It happened in the worker, child-wait, child-dispatch and report-coordination handlers. A segmented simulation's failure was also reported twice: the fan-out logged the segment error, and the worker then reported the wrapping Stage12ExecutionError.

  • A new report_failure helper builds the redacted detail without logging it and emits one report_original_failure record. That record carries the same correlation_id as the persisted Simulation failed (correlation_id=...) summary, plus evaluation_id and, for a simulation, simulation_execution_id and the role. Without a runtime, the one standard-library log line includes the correlation ID.
  • The worker, the segment fan-out, and the coordinator's child-wait, dispatch and report handlers all use it. The worker does not report a segment failure again, because the fan-out has already reported it.
  • failure_detail_from_exception is replaced by redacted_failure_detail, which only describes a failure and never reports it.
  • The expected attributes in Release Stage 12 database sessions and keep original failures visible #789's report-ordering tests now include the identifiers and the correlation ID. Each test also checks that the correlation ID matches the persisted summary.

The Stage 12 precompute app mounts only the read-only Hugging Face secret (eef03c3c)

src/modal/stage12_precompute_app.py runs on every Stage 12 deployment. It mounted the retiring policyengine-data-credentials and huggingface-token (required_keys=["HF_TOKEN"]) Modal secrets on its image build and all five functions.

  • Its image build (policyengine bundle install ... --country us) and all five functions now mount pe-uk-private-hf-read-token with required_keys=["HUGGING_FACE_TOKEN"]. The secret is declared through the same HF_MODAL_SECRET_NAME and HF_RUNTIME_ENV_NAME constants as app.py and v2_app.py. The functions keep stage12-evaluation-gcp-credentials. The app never read HF_TOKEN itself, and nothing else changes.
  • Read scope. Every Hugging Face read the app makes resolves through the US bundle manifest to hf://policyengine/populace-us/.... This covers the bundle install in the image build and any dataset materialization during ensure_datasets or a calculation. PolicyEngine 6.3.4's runtime code names no other Hugging Face repository for the US. policyengine/populace-us is a public, ungated dataset repository. The token's scope covers only the two private UK repositories, and this public repository needs no grant. The same token already downloads these artifacts in the v2 US worker image build. That layer was rebuilt with only this secret in today's 6.3.4 deployment (run 38545891000, staging) and installed populace_us_2024.h5 from policyengine/populace-us. No credential was widened.
  • test_deployment_sources_exclude_legacy_hf_secret_names now includes this app. A new declaration test checks the secret on the image build and every function. Both fail on the previous app source.

Environment variables and wiring

Variable Used by Provider Wiring
HUGGING_FACE_TOKEN Image build (bundle install) and all five functions GitHub organization secret PE_UK_PRIVATE_HF_READ_TOKEN, copied into Modal secret pe-uk-private-hf-read-token The prepare job validates the GitHub value, then runs modal secret create pe-uk-private-hf-read-token HUGGING_FACE_TOKEN=... --env=<modal_environment> --force when deploy_existing_stack or deploy_stage12_v2 is true. deploy_stage12_v2 needs: prepare, and its implicit success() condition means the sync has succeeded, in the same Modal environment, before "Build the independent Stage 12 cache" runs. The sync is in the prepare job, not in the same job as the precompute step. The app attaches the secret to the image build and every function.
GOOGLE_APPLICATION_CREDENTIALS_JSON All five functions (cache bucket access) Secret Manager secret named by vars.STAGE12_GCP_CREDENTIALS_SECRET_NAME, copied into Modal secret stage12-evaluation-gcp-credentials Unchanged: stage12-sync-modal-secrets.sh, in the same deploy_stage12_v2 job, runs before the precompute step.
POLICYENGINE_DATA_FOLDER Functions Image environment Unchanged.
STAGE12_CACHE_BUCKET Local entrypoint on the runner, which passes the bucket to each function GitHub environment variable vars.STAGE12_CACHE_BUCKET Unchanged: precompute step env:.
MODAL_TOKEN_ID, MODAL_TOKEN_SECRET modal secret create and modal run GitHub secrets Unchanged: each step's env:.
  • Both environments. Staging (release_environment: beta, Modal staging) and production (prod, Modal main) call the reusable workflow with deploy_stage12_v2: true and secrets: inherit, from both simulation-deploy.yml and stage12-deploy.yml.
  • Modal secret names. A read-only modal secret list showed pe-uk-private-hf-read-token and stage12-evaluation-gcp-credentials in both staging and main, each used on 2026-10-14. Secret values and the GitHub secret were not inspected.
  • Not verified directly. Whether PE_UK_PRIVATE_HF_READ_TOKEN is granted to this repository and is valid. Today's successful deployments, which validate and synchronize it, indicate that it is. The precompute app has not run with the new secret, because no deployment was run.

Rebased onto main after #789

The branch is rebased onto 5484278455e7fd48bb8d874f0e26245895d2730c, the merge of #789. The earlier merge commit from main is gone, and each commit of this PR now builds on #789.

Connection opener

  • Stage12PersistenceStore.replace_simulation_if_status, which this PR adds, opens its connection through _transaction, like every other store operation. Only the connect step can be retried; its statements and commit are never retried.
  • The coordinator and the simulation worker build their stores with runtime_store(runtime), so a refused connection is retried and each retry is logged with the operation name. The Cloud Run entrypoint keeps the non-retrying default.

Failure recording

  • Each failure is reported to telemetry once, through Release Stage 12 database sessions and keep original failures visible #789's report_original_failure, before any database write. This covers worker failures, segment failures, child dispatch failures, child failures seen while observing, and report failures. See "Fixes after review" for how the report now carries the correlation ID.
  • Report and worker failure records are written through persist_failure_record, which logs a failed write instead of raising it. A failed simulation's own record is updated by the coordinator through _record_child_failure, which logs a failed read or write with the report and simulation identifiers, like the cancellation records.
  • This PR's structured failure details are kept. The records now hold the redacted summary with a correlation ID, or the typed public error, instead of the exception type name. Release Stage 12 database sessions and keep original failures visible #789's tests were adapted to these summaries and to the conditional replace_simulation_if_status writes.
  • A child dispatch failure, a child failure and a closed wait window each raise Stage12ExecutionError after being reported where they occur. The report handler therefore reports only other errors, so each failure is reported once.
  • A closed wait window is now reported too, with failure scope child_wait_timeout, before the cancellation records and the report record are written. A new test makes those writes fail and checks that the report still reached telemetry first.
  • Release Stage 12 database sessions and keep original failures visible #789's _mark_child_failed is folded into this PR's _replace_running_simulation, which the dispatch, child-failure and cancellation paths share. It only changes a running record, which this PR requires.
  • Cancellation records keep this PR's own handling. A failed cancellation write is logged with evaluation_id and simulation_execution_id, and an unfinished child is then cancelled through Modal. They do not use persist_failure_record, because the coordinator must know whether the record was written.

Known open issue (fixed after review)

The coordinator read a failed child's record outside the failure-recording guard, so an error on that read replaced the child's failure. This is fixed in b6c691d1; see "Fixes after review". #789's test for this case, test_coordinator_reports_a_child_failure_once_when_its_record_is_unwritable, is no longer marked xfail and passes.

Connection retries and the finishing time

#789 retries a refused connection for up to 60 s. The last attempt can start just before 60 s and then wait up to the 10 s connect timeout, so one store operation can take about 70 s when the pooler refuses throughout. After the wait window closes:

  • Wait timeout: at most 5 operations (a read and a conditional write for each unfinished child, then the report record), so about 350 s. This path has no production-result wait, so 1,500 s remain.
  • Child failure at the last poll: at most 6 operations, so about 420 s, which also fits.
  • Direct report succeeds: 1 operation (2 if that write fails), so 70 to 140 s of the 600 s.
  • Automatic (shadow) report succeeds: at most 5 operations outside the 900 s production-result wait, so about 350 s. That leaves about 250 s of the 600 s to read the artifacts, aggregate and write. A complete testing-environment US report took 288 s including every calculation, so the budget still holds, but in this worst case the margin falls from 600 s to about 250 s.

If the coordinator ever exceeded 5,400 s, Modal would stop it and the report would stay running without a failure record. The timeouts are unchanged in this rebase.

Replaces #793 and covers #792

This PR replaces #793 and adopts its practices. Issue #792 asks for one shared wait deadline that fits inside the coordinator's timeout, and for every unfinished worker to be cancelled and recorded as failed. Observing baseline and reform concurrently was always part of this PR.

Wait window and timeouts

This PR follows #793's rule: the shared wait equals the workers' Modal timeout, and the coordinator's timeout is that wait plus 900 s for the production-result wait plus 600 s to finish.

  • One shared window from coordinator entry. The window opens when coordinate_report starts. It covers claiming, output planning (including loading the country model), retained-artifact reads, dispatch, and the concurrent wait for both simulations. Setup time therefore shortens the simulations' window, not the time the coordinator needs afterwards.
  • Window = longest simulation worker timeout = 3,900 s. This is the US worker's timeout, and it covers the UK worker (3,000 s) and each US region-group call (3,600 s). Before this update, the window was 3,000 s, so a US simulation could never use the 3,900 s Modal allowed it.
  • Coordinator timeout = 3,900 + 900 + 600 = 5,400 s (was 5,100 s):
    • 3,900 s: shared wait window;
    • 900 s: production-result wait (PRODUCTION_RESULT_WAIT_TIMEOUT_SECONDS);
    • 600 s: read and validate both simulation artifacts, aggregate, write the aggregate and comparison artifacts, and record the outcome or failure.
  • Why 600 s is enough. A complete testing-environment US report took 288.2 s, including all 40 region-group calculations, merging, reads, aggregation and writes. A failed report took 26.8 s. So 600 s is ample, and I kept Share one Stage 12 child wait deadline and cancel unfinished children #793's value. See "Connection retries and the finishing time" above for the worst case with Release Stage 12 database sessions and keep original failures visible #789's retries.
  • No overrun. The coordinator's last poll no longer sleeps past the deadline, so the window closes on time rather than up to 1 s late, and the 600 s is not eroded.
  • Named constants. A new dependency-free module, stage12_timeouts.py, defines the worker timeouts, the window and the finishing time, and derives the coordinator timeout from them. The Modal app declaration, the coordinator and the comparison step all read it.
  • Dependents. I searched for anything that relies on the old 5,100 s or 3,000 s values. The Modal app declaration, its declaration test, the coordinator comment and the docs did, and are updated. The entry service, the persistence library, deployment workflows and scripts, and release validation do not depend on them. I also checked the Public API's Stage 12 code in a local clone. It has no dependency on these timeouts, and its status rules let a simulation record move from running to failed and from failed back to running, so failed cancellation records and retries still fit.
  • Environment variables. Only the timeout values of run_single_simulation_us, run_single_simulation_uk and coordinate_report change. Their images, worker_secrets (the GCP, Hugging Face and comparison-runtime Modal secrets) and environment are unchanged. The new constants module ships in the existing policyengine_simulation_executor package source that every Stage 12 image already includes. No CI, Docker or Terraform changes.

Cancellation

  • Recorded as failed. Every simulation the coordinator cancels is recorded as failed with simulation_cancelled. Its error_summary gives the reason:

    • "Cancelled after the baseline simulation failed." (or reform);
    • "Cancelled after the 3900-second wait window closed.", where the report itself fails with simulation_wait_timeout;
    • "Cancelled after the coordinator failed."

    The earlier incomplete status and the cancelled_after_peer_failure and cancelled_after_coordinator_failure codes are gone. Nothing else needed them: workers treat any status other than running as a cancellation, and the entry service passes statuses and codes through unchanged.

  • Only running records change. A simulation that has already succeeded or failed keeps its own outcome.

  • Always stopped in Modal. An unsegmented simulation is always cancelled through Modal, even if its record could not be written. A segmented simulation reads its failed record, cancels its own region-group calls and exits. Modal does not cancel spawned calls recursively, so the coordinator cancels a segmented simulation through Modal only if it could not write the record.

  • Startup failures too. Any coordinator failure after the first spawn stops every started, unfinished simulation. Examples are a failed dispatch placeholder write, a failed invocation attach (including the simulation whose attach failed), a failed restore and a retained-artifact error. A call restored from an earlier coordinator attempt is stopped too. Before this update, only a failed spawn did this.

  • Errors never mask the original failure. Cancellation and state-update errors, including errors reading or writing the failed simulation's own record, are logged, and the report keeps the original failure.

  • Traceable logs. When recording or requesting a cancellation fails, the log includes evaluation_id and the simulation's simulation_execution_id, in the message and as record attributes, alongside the role and error type. The error message itself is not logged. Share one Stage 12 child wait deadline and cancel unfinished children #793 used event-name messages with these fields only as attributes. This PR also puts them in the message, because the observability log handler exports only the message.

Carried over from #793

  • The timing rule and its declaration test, which reads the real Modal timeouts through fixtures.fake_modal, and the setup-time and exhausted-window tests.
  • simulation_cancelled failed records, the running-only guard, and Modal cancellation even when the state write fails.
  • Tests: a cancelled and recorded reform, a finished baseline left alone, a restored reform call cancelled on retry, a sibling that already succeeded or failed left unchanged, every unfinished child cancelled on timeout, and cancellation and state errors that do not mask the failure.
  • ImmediateCall.cancel now counts cancellations.
  • A changelog line. changelog_entry.yaml keeps Release Stage 12 database sessions and keep original failures visible #789's line and this PR's line.
  • Not carried over: Share one Stage 12 child wait deadline and cancel unfinished children #793's tests of sequential call.get timeouts, because this PR polls both simulations without blocking.

Validation of the review fixes

  • Full executor suite on Python 3.13, with CI's uv sync --extra test environment: 1149 passed, 22 skipped, 3 deselected, 0 xfailed (was 1134 passed and 1 xfailed).
  • Stage 12 executor tests (tests/test_stage12_*.py): 213 passed.
  • Simulation entry suite: 381 passed. Stage 12 persistence suite: 19 passed.
  • New tests, each run against the previous source and failing there:
    • the failed simulation's own record read or write fails once, in the child-wait path (2 cases) and in the dispatch-failure path (2 cases);
    • each unexpected failure is reported exactly once, with a runtime and without one, in the worker, child-wait, dispatch and report paths (8 cases), plus a segment failure (1 case);
    • the precompute app mounts only the read-only secret (1 test), and the guard now includes this app.
  • test_coordinator_reports_a_child_failure_once_when_its_record_is_unwritable no longer has its xfail marker and passes. Its expected report attributes now include the identifiers and the correlation ID.
  • Pinned Ruff (0.15.22) format --check and check on the 10 changed Python files: clean. Ruff format --check on every projects/*/src and libs/*/src directory: clean. git diff --check: clean.
  • Not run: new testing, staging or production deployments.

Merged main after #791

#791 stores a redacted failure cause: the type, the stage, the simulation role and the message. For every unexpected error, this PR stored a generic message with a correlation ID instead. Main is merged in as a merge commit rather than a rebase, because 16 of this PR's 25 commits touch the three files that conflicted. This PR keeps its own failure model and uses #791's cause within it:

  • Unexpected errors: redacted_failure_detail stores the redacted cause, followed by the correlation ID, for example OperationalError in stage12_child_state_create: ... (correlation_id=3f9c...). The cause is shortened, never the ID, to fit the 512-character summary. Typed input and SPM failures, failures a worker already recorded, and cancellations are unchanged.
  • Stages and roles: the coordinator and the workers open their spans and operations through Record a redacted Stage 12 failure cause instead of only the exception class #791's tracked_stage, so a failure names its stage. The coordinator waits on both simulations at once, so it records each one's role while checking it.
  • Telemetry: the original error is still reported once, with its correlation ID. As on main, the redacted cause is also attached to the failing operation, with the original stack.
  • Error codes: the report keeps this PR's codes. For example, a failed child gives the report simulation_invocation_failed, where Record a redacted Stage 12 failure cause instead of only the exception class #791 always used report_coordination_failed.
  • Tests:
    • Tests that expected the generic message, or no message at all, now expect the redacted cause.
    • Record a redacted Stage 12 failure cause instead of only the exception class #791's six cause tests are ported with this PR's error codes.
    • A new test checks that a long cause is shortened and keeps its correlation ID.
    • The exactly-once telemetry tests now check for one log of the original error and one recorded redacted cause.
  • Docs: the README and the Stage 12 migration notes describe the new summary.
  • Validation: executor suite: 1230 passed, 22 skipped, 3 deselected. Simulation entry suite: 449 passed. Stage 12 persistence suite: 19 passed. Quality guards, pinned Ruff format --check on every src directory, and git diff --check all pass.

Rebased onto main after #762, #763 and #795

  • Move the deploy pipelines' multi-line steps into tested scripts #763 moved the worker validation step into .github/scripts/stage12-validate-v2-workers.sh, which conflicted with this PR's --cache-manifest-sha256 argument. The script now takes the digest as a second required argument and passes it to validate_v2_worker_release. The workflow passes steps.stage12_cache.outputs.manifest_digest, the same pattern Move the deploy pipelines' multi-line steps into tested scripts #763 uses to pass step outputs to its other scripts.
  • The digest comes from the Build the independent Stage 12 cache step (stage12-cache-precompute.sh), which runs earlier in the same deploy_stage12_v2 job and writes manifest_digest to GITHUB_OUTPUT. RUNNER_TEMP and GITHUB_OUTPUT come from the runner. MODAL_TOKEN_ID and MODAL_TOKEN_SECRET come from GitHub Actions secrets, unchanged.
  • test_deploy_workflow_scripts.py checks that the workflow passes the digest and that the script forwards it. A new test checks that the script fails, without calling uv, when the digest is missing.
  • Simulation entry suite: 449 passed. Full executor suite: 1169 passed, 22 skipped, 3 deselected. Quality guards (rules-registry, ci-scripts), pinned Ruff and bash -n pass.

Rebased onto main after #757 and #787

Rebased onto the rules registry (#752)

  • Rebased onto main after Add a rules registry that makes every enforced and unenforced rule visible #752. Code and the changelog line are unchanged from the previous head (eef03c3); the only differences are main's changes and the registry entry below.
  • The rules-registry guard from Add a rules registry that makes every enforced and unenforced rule visible #752 flagged test_stage12_only_deployment_skips_existing_stack_integration_suite, which this PR adds, as a guard test no rule references. It is now one of the nodes of sim.stage12.deployment-scope. The rule's statement now also says that a Stage 12-only deploy never runs the existing stack's complete integration suite. docs/generated/rules.md and the AGENTS.md Rules block were re-rendered.
  • python -m scripts.rules guard --base-ref origin/main, scripts/run_quality_guards.py and the rules tool's own tests pass. tests/test_deployment_assets.py: 113 passed.

Validation after rebasing onto #789

  • Executor environment synced with CI's uv sync --extra test, plus --locked to confirm the lockfile is current: PolicyEngine 6.3.4 (policyengine-us 2.2.1, policyengine-uk 2.126.12, policyengine-core 3.33.0).
  • Full executor suite on Python 3.13: 1134 passed, 22 skipped, 3 deselected, 1 xfailed (the known open issue above).
  • Stage 12 executor tests (tests/test_stage12_*.py): 198 passed, 1 xfailed.
  • Simulation entry suite: 381 passed. Stage 12 persistence suite: 19 passed.
  • Every rebased commit was checked out in turn and its Stage 12 executor tests and persistence suite were run. They pass from Reconcile segmented Stage 12 execution with current typed provenance onwards. The four commits before it fail one worker-validation test, test_us_validation_normalizes_the_frontend_reform_interval, exactly as they did before this rebase: main's reform-interval normaliser predates them, and that commit updates the test.
  • Regression check: removing the closed-window report makes the new timeout test fail.
  • Pinned Ruff (0.15.22) format --check and check on the 49 changed Python files: clean. Ruff format --check on every projects/*/src and libs/*/src directory, as CI runs it: clean. git diff --check: clean.
  • Not run: new testing, staging or production deployments.

Validation of the cancellation and timeout update (before rebasing onto #789)

  • Full executor suite on Python 3.13 with uv run --frozen: 809 passed, 23 skipped, 3 deselected. Before that update it was 786 passed.

  • Stage 12 executor tests (tests/test_stage12_*.py): 181 passed.

  • tests/test_stage12_runtime.py gained 23 tests in that update, covering startup cancellation, cancellation records, Modal fallback, logs, and timing.

  • Regression checks. I reverted each behaviour in turn, and each time the matching new tests failed:

    • removing the Modal fallback after a failed state write;
    • letting the last poll sleep past the deadline;
    • overwriting a sibling that already finished;
    • anchoring the deadline at observation instead of coordinator entry.

    The startup-cancellation, cancellation-log and coordinator-entry tests also failed against the head before that update.

  • Simulation entry suite: 162 passed. Stage 12 persistence suite: 14 passed. Their cancellation fixtures now use the failed simulation_cancelled record.

  • Scoped Pyright on the coordination, comparison and timeouts modules: 0 errors.

Prior validation before the earlier rebases

  • Stage 12 simulation executor tests: 130 passed, with 1 warning.
  • Stage 12 simulation entry tests: 81 passed.
  • Stage 12 persistence tests: 14 passed.
  • Ruff lint checks passed for the changed Python surface.
  • Repository-wide Ruff formatting found one pre-existing formatting difference in projects/policyengine-simulation-executor/tests/test_simulation_output_geographic.py, which this branch does not modify.
  • Deployed and validated all seven Stage 12 functions in the Modal testing environment, including US and UK worker validation and a cache-hit check for the 20-segment, three-year cache.
  • Completed a testing-environment US report with 1 coordinator call, 2 simulation calls, and 40 successful segment calls in 288.2 seconds.
  • Completed an invalid-reform report in 26.8 seconds: the reform retained invalid_policy_period, the baseline became incomplete with cancelled_after_peer_failure (the behaviour at that commit; a cancelled baseline is now failed with simulation_cancelled), and the report failed with the original validation summary.

Earlier testing-environment deployment note

A fresh UK dataset download was blocked because the configured Hugging Face credential could not access the private UK dataset. The testing deployment reused the previously validated UK and coordinator images from the same commit and rebuilt the US image layer with the validated Stage 12 cache.

Earlier current-main reconciliation

The executor keeps main's PolicyEngine 6.3.4 requirements and frozen lockfile; it does not restore older country packages.

Conflict resolution preserves main's output-planning instrumentation and UK metadata validation while retaining concurrent baseline/reform monitoring and segmented US calculation.

Segment merging accepts the current typed SPMProvenance contract. Every segment must supply the same complete receipt, including configuration, forecast hash, years and runtime versions; incomplete or inconsistent receipts fail. The merged logical simulation keeps one compact receipt, not a legacy spm_config object or a growing receipt list. Five regressions cover preservation, missing receipts and mismatched scenario, runtime versions and years.

The worker-validation unit test expects the complete reform date interval, matching main's normalizer rather than discarding its end date.

The earlier live results above describe an earlier commit on an older PolicyEngine version, not a fresh 6.3.4 qualification. No deployment has been run on .py 6.3.4 or any later version; the branch is now on 6.7.5.

🤖 Generated with Claude Code

@anth-volk
anth-volk force-pushed the feat/stage12-segmentation-cache branch 2 times, most recently from d5f6837 to 16c7701 Compare October 1, 2026 12:30
@anth-volk
anth-volk force-pushed the feat/stage12-segmentation-cache branch from 16c7701 to 069ec46 Compare October 12, 2026 12:32
@anth-volk
anth-volk force-pushed the feat/stage12-segmentation-cache branch 4 times, most recently from f22d344 to f510ed9 Compare October 14, 2026 20:24
anth-volk and others added 21 commits October 15, 2026 01:55
Cancellation state-update and cancel-request failures previously logged
only the simulation role and error type. They now also log the report's
evaluation_id and the child's simulation_execution_id, in the message and
as record attributes, and still omit the error's own message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a failed spawn used to cancel already-started children. Any other
coordinator failure after the first spawn, such as a failed dispatch
placeholder write, a failed invocation attach, a failed restore, or a
retained-artifact error, reached the report failure handler, which marked
the report failed but left every started worker running until its own
timeout.

The handler now stops every started, unfinished child with the existing
cancellation path: it records the child as incomplete with
cancelled_after_coordinator_failure, cancels an unsegmented call, and
lets a segmented call stop itself once it reads that record. Children
that already finished are left unchanged. Child, dispatch and wait-timeout
failures keep their own peer cancellation, and cancellation or
state-update errors are logged without replacing the original failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The child-wait deadline used to start when observation began, after
claiming, output planning (including the country model load),
retained-artifact reads and dispatch. Setup time therefore came out of
the coordinator's remaining Modal time rather than the children's
window.

The 3,000 s window now opens when coordinate_report starts, so setup
shortens the children's window and the coordinator always keeps 2,100 s
of its 5,100 s Modal timeout after the window: up to 900 s for the
production-result wait and 1,200 s to read results, aggregate, write
artifacts and record the outcome. The timeout summary now says the
window is measured from the coordinator's start.

A new test runs the coordinator with 0, 400 and 2,000 s of simulated
setup, checks that the window closes 3,000 s after entry, and checks the
declared Modal timeouts still leave the production wait plus at least
600 s.

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

The coordinator stopped waiting 3,000 s after it started, although a US
simulation worker may run for 3,900 s and each of its region-group calls
for 3,600 s, so US simulations could never use the time Modal allowed
them. The coordinator's 5,100 s timeout was not derived from either.

Following the rule from #793, a new dependency-free stage12_timeouts
module defines the worker timeouts, sets the shared child-wait window to
the longest of them (3,900 s), and derives the coordinator's Modal
timeout as that window plus the 900 s production-result wait plus 600 s
to read results, aggregate, write artifacts and record the outcome:
5,400 s. A complete testing-environment US report took 288 s, so 600 s
of finishing time is ample. The Modal app, coordinator and comparison
step all read these constants.

The last poll no longer sleeps past the deadline, so the window closes
on time instead of up to 1 s late and the finishing time is not eroded.

Tests read the declared Modal timeouts and check that the window covers
every simulation worker's timeout and that the coordinator's timeout
leaves the production wait plus at least 600 s. They also check that
setup time shortens the window and that an exhausted window gives the
children no extra time. Only the Modal timeout values change: no image,
secret or environment variable attachment changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adopt #793's cancellation practice in the concurrent coordinator:

- Every simulation the coordinator stops, after a peer failure, a wait
  timeout or a coordinator failure, is recorded as failed with
  simulation_cancelled, and its error_summary gives the reason, for
  example "Cancelled after the baseline simulation failed.". The
  cancelled_after_peer_failure and cancelled_after_coordinator_failure
  codes and the incomplete status for cancellations are gone; the wait
  timeout stays the report's error code.
- Only a running record changes, so a simulation that already succeeded
  or failed keeps its outcome. Workers already treat any status other
  than running as a cancellation, so a segmented worker still sees the
  failed record and cancels its own region-group calls.
- An unsegmented simulation is always cancelled through Modal, even when
  its record could not be written. A segmented simulation is cancelled
  through Modal only when its record could not be written, because Modal
  does not cancel spawned calls recursively. Cancellation and
  state-update errors are still logged without replacing the original
  failure.

Carry over #793's tests for a cancelled and recorded reform, a finished
baseline left alone, a restored call cancelled on retry, finished
siblings left unchanged, and both unfinished children cancelled on
timeout. Add tests for the Modal fallback after a failed state write,
for segmented and unsegmented simulations. ImmediateCall now counts
cancellations, and the entry and persistence fixtures use the new
cancellation record. Add a changelog line, as #793 did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rebase onto #789 reports a closed child-wait window to telemetry
before cancellation and report failure records are written, as every
other Stage 12 failure is. This test makes those writes fail and checks
that the timeout still reached telemetry first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 19, 2026
#712 landed on main after this stack branched. Before its C901
merge_segment_results is split, pin the checks its tests did not reach:
a result set that omits a segment, a result naming another region group,
segments with different entities or column dtypes, and an entity without
its identifier. The serializer refuses frames without an identifier, so
that pin decodes every segment to one directly.

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

C901 and PLR0912 flag merge_segment_results. It now indexes the results
(rejecting an incomplete set or a duplicate index), decodes each planned
segment in partition order (rejecting an omitted segment or one naming
another role or region group), and merges each entity's frames
(rejecting different entities, dtypes, a missing identifier or duplicate
IDs), each in its own function. The checks and their order are
unchanged, and the previous commit's pins keep their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 19, 2026
#712 landed on main after this stack branched and extended
run_single_simulation, which C901 and PLR0915 now flag. Before it is
split, pin the paths its tests did not reach: a simulation that is no
longer active is refused, a claim another writer won stops the run as
cancelled, and a failed status read in the cancellation check is logged
by error type only and treated as not cancelled, so the run completes.

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

C901 and PLR0915 flag run_single_simulation, which #712 extended with
segmented calculation and cancellation. It now reads as its phases: claim
the simulation (refusing one that is no longer active or that another
writer claimed), build the cancellation check, write the input,
calculate (segmented, standard or injected, always as a
SimulationCalculation), write the artifact, and record success or
failure, each phase but the writes in its own function. The order of
reads, writes and reports is unchanged, and the previous commit's pins
keep their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 19, 2026
#712 landed on main after this stack branched. Before its C901, PLR0912
and PLR0915 fetch_stage12_cache is split, pin what its two tests did not
reach: each manifest rejection (no bucket or manifest, schema, bundle,
year set, artifact list, artifact count and deployment digest), each
artifact rejection (a non-object entry, a filename with a directory, a
duplicate filename, a non-integer size and a same-size file with another
digest, which leaves no partial file), and the storage client built with
no credentials, a credentials object or credentials encoded as a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 19, 2026
…d file steps

C901, PLR0912 and PLR0915 flag fetch_stage12_cache. It now validates the
whole manifest first (schema, installed bundle and years; the artifact
list and its counts; then the deployment digest), builds the storage
client from the deployment's credentials only when none is given, and
fetches each artifact through a partial file. The checks, their order
and the environment variables it reads are unchanged; the module stays
self-contained for the image build. The previous commit's pins keep
their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
…nd aggregate phases

Group the coordinator's Modal identity and its injectable seams, and split
submitted-parent validation into invocation, claim-state and production
checks. The Modal coordinate_report function keeps its signature.

Rebased onto #712, the phases carry #712's coordinator: one shared
child-wait window opened before any setup, nonblocking probes of both
children, cancellation of the unfinished children on any failure, and
failure details with correlation IDs. CoordinatorDependencies gains
#712's sleep, monotonic and wait_timeout_seconds seams. The report-level
failure handling moves into _ReportCoordination.fail, and the run's child
plans, calls and descriptors live on _ReportCoordination, so a failure in
any phase can stop the started children. The coordination pins expect
#712's single child-wait span and correlation-ID suffixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#712 landed on main after this stack branched. PLR2004, which this PR
enables, flags five magic values in its code; each is now a named
constant, with no behaviour change:

- the date-range period's two parts (_DATE_RANGE_PART_COUNT, the name
  simulation_runtime already uses);
- the segment poll's consecutive-error limit (CONSECUTIVE_POLL_ERROR_LIMIT,
  as in segmented_national);
- the national partition's 20 region groups
  (NATIONAL_PARTITION_GROUP_COUNT, next to US_REGION_GROUPS);
- worker validation's cache years, artifact count and probe year, from
  the cache precompute's PRECOMPUTE_YEARS: one dataset per year and one
  baseline per US region group per year make the 63 artifacts.

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

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its eleven-argument _observe_simulations. It becomes
_ReportCoordination._observe_children, which takes the calls and
descriptors and reads the child plans, store, runtime, progress, stage
plan, clock, sleep and wait window from the coordination it belongs to.
The polling, failure and timeout logic is unchanged, and #712's runtime
tests cover every path through it.

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

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its seven-argument report_failure. The telemetry scope,
default error code, attributes and segment index that say where a
failure was caught now travel as one FailureSite; the error, runtime and
progress stay separate. The coordinator's three calls, the worker's and
the segment runner's pass the same values as before.

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

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags Stage12SegmentRunner's nine-argument constructor. The
Modal module, region groups, sleep and the two poll intervals that tests
replace now travel as one SegmentRunnerSeams, whose defaults are the
deployed ones; the simulation, app name, runtime and cancellation check
stay separate. run_segmented_simulation passes nothing new.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
…s one group

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its eight-argument build_stage12_simulation. The country
module, region, dataset selection and loaded dataset the worker resolves
in its own stages now travel as one ResolvedSimulationInputs. The builder
reads the scoping strategy from the region, as _build_simulation already
does, so it no longer takes the region code it only deleted. The SPM
parameters, normalised policy and execution input stay separate.

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

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags the cache precompute's seven-argument run_precompute. Its
five Modal function handles now travel as one CachePrecomputeFunctions,
as the baseline precompute's PrecomputeFunctions already do; the bucket
and echo stay separate. The Modal app's entrypoint passes the same five
functions, and its secrets and environment are unchanged.

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

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags the six-argument ScriptedChildCall its runtime tests use.
The result, wait error, cancel error and during-wait hook that script a
call's outcome now travel as one ChildCallScript; the call ID and clock
stay separate. Both constructions script the same outcomes as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#712 landed on main after this stack branched. Before its eight-return
qualifying_cache_identity is split, pin the ineligible cases its tests
did not reach: no or a non-region-group scoping strategy, a missing,
empty or non-list region group, a non-state or non-text region code, and
a scope without a cache key. Each gets no cache identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
… and scope checks

PLR0911 flags the eight returns of qualifying_cache_identity. It now
asks three questions: is this a current-law baseline on the default
dataset, what is its group of state regions, and what is the
region-group scope's cache key. Each check is pure, so asking them in
this order gives the same result; the previous commit's pins and
#712's identity tests keep their assertions.

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

#712 landed on main after this stack branched. Before its C901
verify_determinism_impl is split, pin what its tests did not reach: every
kind of frame difference (an entity missing on one side, a column on only
one side, a dtype change and a value change) in its sorted order, the
rejection of a probe that is not a planned baseline, and that the stored
baseline is downloaded only when it is not already in the data folder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
C901 flags verify_determinism_impl. The comparison of cached and fresh
frames moves into _frame_differences, which lists each difference in the
same sorted order; the probe check, download, cache-hit check and pass
record stay in verify_determinism_impl. The previous commit's pins keep
their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#712 landed on main after this stack branched. Before its C901
merge_segment_results is split, pin the checks its tests did not reach:
a result set that omits a segment, a result naming another region group,
segments with different entities or column dtypes, and an entity without
its identifier. The serializer refuses frames without an identifier, so
that pin decodes every segment to one directly.

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

C901 and PLR0912 flag merge_segment_results. It now indexes the results
(rejecting an incomplete set or a duplicate index), decodes each planned
segment in partition order (rejecting an omitted segment or one naming
another role or region group), and merges each entity's frames
(rejecting different entities, dtypes, a missing identifier or duplicate
IDs), each in its own function. The checks and their order are
unchanged, and the previous commit's pins keep their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#712 landed on main after this stack branched and extended
run_single_simulation, which C901 and PLR0915 now flag. Before it is
split, pin the paths its tests did not reach: a simulation that is no
longer active is refused, a claim another writer won stops the run as
cancelled, and a failed status read in the cancellation check is logged
by error type only and treated as not cancelled, so the run completes.

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

C901 and PLR0915 flag run_single_simulation, which #712 extended with
segmented calculation and cancellation. It now reads as its phases: claim
the simulation (refusing one that is no longer active or that another
writer claimed), build the cancellation check, write the input,
calculate (segmented, standard or injected, always as a
SimulationCalculation), write the artifact, and record success or
failure, each phase but the writes in its own function. The order of
reads, writes and reports is unchanged, and the previous commit's pins
keep their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
#712 landed on main after this stack branched. Before its C901, PLR0912
and PLR0915 fetch_stage12_cache is split, pin what its two tests did not
reach: each manifest rejection (no bucket or manifest, schema, bundle,
year set, artifact list, artifact count and deployment digest), each
artifact rejection (a non-object entry, a filename with a directory, a
duplicate filename, a non-integer size and a same-size file with another
digest, which leaves no partial file), and the storage client built with
no credentials, a credentials object or credentials encoded as a string.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
…d file steps

C901, PLR0912 and PLR0915 flag fetch_stage12_cache. It now validates the
whole manifest first (schema, installed bundle and years; the artifact
list and its counts; then the deployment digest), builds the storage
client from the deployment's credentials only when none is given, and
fetches each artifact through a partial file. The checks, their order
and the environment variables it reads are unchanged; the module stays
self-contained for the image build. The previous commit's pins keep
their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
…PERF, PLW, PT, PTH, RUF, SIM and others) (#802)

* Exempt the rules tool's unittest tests from pytest-style rules

The shared rules tool needs only the standard library, so its tests use
unittest on purpose. Registered as sim.quality.unittest-ruff-exemptions.

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

* Resolve ruff group 8a findings in policyengine-fastapi

- JWTDecoder takes auto_error as keyword-only (FBT001/FBT002); the only
  caller, the gateway, already passes it by keyword.
- JWTDecoder.__call__ declares its bearer dependency with Annotated (B008).
- The 403s raised while validating a token chain the caught error (B904).
- Iterate dicts directly instead of over .keys() (SIM118).
- Name the ignored error code on the trace-filter type: ignore (PGH003).
- Explain NoOpObservability.span's unused arguments: it implements the
  Observability protocol (ARG002).
- Check expected exceptions with pytest.raises(match=...) (PT017).

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

* Resolve ruff group 8a findings in the simulation contract

- Merge the two branches that both fall back to the package version
  (SIM114), and collapse the nested as_of check (SIM102).
- The test modal.Dict stand-in takes create_if_missing as keyword-only,
  matching Modal's own from_name (FBT001/FBT002).
- Request the mock_modal fixture with usefixtures, and prefix the fake
  HF fetchers' unused positional parameters with an underscore (ARG).
- Use tuples for parametrize names (PT006), a dict literal (C408), and
  match the expected validation messages (PT011).

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

* Resolve ruff group 8a findings in the entry project

- CallerAuthenticator reads its bearer dependency marker from a
  module-level default (B008). The Annotated form isn't usable there:
  the module postpones annotations and FastAPI resolves them against the
  callable's __globals__, which a callable instance lacks. The OpenAPI
  document is unchanged.
- Keep the three telemetry try/except blocks, whose except lines carry
  the BLE001/S110 reasons, and suppress SIM105 on their try lines.
- Mark the failing test backends' request() with @OverRide, prefix the
  positional-only parameters of the _decoder replacements with an
  underscore, and explain the fake decoders' unused parameters (ARG).
- Drop the unused tmp_path requests (ARG001) and use tuples for
  parametrize names (PT006).

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

* Resolve ruff group 8a findings in the gateway project

- require_auth declares its bearer dependency through a module-level
  Annotated alias (B008); the module postpones annotations, and FastAPI
  resolves the alias through the function's globals. The OpenAPI
  documents are unchanged.
- Chain the caught error into the 400, 404 and 503 HTTPExceptions and the
  route-resolution ValueErrors (B904). A BLE001 suppression that the
  chained raise made unused keeps its reason as a comment.
- Keep the telemetry try/except blocks, whose except lines carry the
  BLE001/S110 reasons, and suppress SIM105 on their try lines.
- Use a conditional expression for the resolved version (SIM108) and
  Path.open in the OpenAPI generator (PTH123).
- Test stand-ins: keyword-only create_if_missing and use_bundle_default
  (FBT), ClassVar registries (RUF012), and explained unused parameters
  of the FunctionCall.get stand-in and the side-effect fixture (ARG).
- Tests: usefixtures for mock_modal and the import-isolation fixture,
  tuple parametrize names, bare @pytest.fixture, a split assertion, an
  explicit check=False, dict() over a comprehension, and the decoder
  class passed directly instead of through a lambda.

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

* Resolve ruff group 8a findings in the executor sources and fixtures

- Assign spm directly instead of through setattr (B010), use
  dict.fromkeys (C420), list unpacking (RUF005), one combined
  with-statement in the same enter/exit order (SIM117), a membership
  test (SIM109), merged upper-casing branches (SIM114), extend over an
  append loop (PERF401), and Path.open/Path.unlink (PTH).
- Keep, with reasons: the per-child and per-call try/except loops
  (PERF203), the credentials temp file that stays open across the
  yield (SIM115), HDFStore.keys(), which isn't a dict view (SIM118),
  the FastAPI lifespan hook's app parameter (ARG001), and the precompute
  local entrypoint's positional force flag, whose signature Modal turns
  into the CLI (FBT001/FBT002).
- Keep the telemetry and best-effort-cancel try/except blocks, whose
  except lines carry the BLE001/S110 reasons, and suppress SIM105 on
  their try lines.
- Fixtures: prefix the identity stubs' positional-only parameters and
  the fake local_entrypoint's **kwargs with an underscore (ARG).

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

* Resolve unused-argument findings in the executor tests

- Request side-effect fixtures with @pytest.mark.usefixtures, and drop
  an unused monkeypatch request (ARG001/ARG002).
- Prefix unused parameters of monkeypatched replacements with an
  underscore, after checking that every caller passes them positionally,
  and rename unused *args/**kwargs to *_args/**_kwargs (ARG001/ARG005).
- Keep, with reasons, parameters that callers pass by keyword: the
  _load_dataset and _reduce replacements, the fake _build_simulation and
  economic_impact_analysis keyword-only parameters, FunctionCall.get
  stand-ins' timeout, and the Stage 12 writer stand-ins' prefix. Fake
  model versions that duck-type .py's MicrosimulationModelVersion keep
  their parameters too.
- Mark the failing child store's create_or_resolve_simulation with
  @OverRide.
- Use tuples for parametrize names (PT006).

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

* Resolve the remaining ruff group 8a findings in the executor tests

- Pass booleans by keyword: DatasetSelection's is_default (FBT003), and
  keyword-only routed/accepted test parameters, the routed helper flag,
  the output-builder helper's options and the modal.Dict stand-ins'
  environment_name/create_if_missing, matching Modal (FBT001/FBT002).
- Match the real messages in pytest.raises (PT011); one native-only
  check keeps its broad ValueError, with a reason, because the
  rejecting layer's message varies. Make regex patterns raw or escaped
  (RUF043), give pytest.fail a message (PT016), and move the GCP
  credential cleanup check into a helper so pytest.raises wraps one call
  (PT012/SIM117).
- Use Path methods (PTH), literals and dict() (C408/C416), a list
  literal (SIM905), unpacking (RUF005), extend (PERF401), ClassVar
  annotations (RUF012), an underscore for an unused unpacked value
  (RUF059), explicit check=False (PLW1510) and an ASCII operator in a
  comment (RUF003).
- Pass regions.get, checks.append and nullcontext directly instead of
  through lambdas; nothing patches them (PLW0108).

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

* Resolve ruff group 8a findings in the CI script and integration tests

- The package update script stops overwriting its loop variable
  (PLW2901) and replaces the pyproject through Path.replace (PTH105).
- The integration conftest uses bare @pytest.fixture (PT001).

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

* Enable ruff group 8a (arguments, idioms and pytest style)

Adds ARG, ASYNC, B, C4, FBT, PD, PERF, PGH, PLW, PT, PTH, RUF and SIM to
the selected rule families. Every finding outside the shared rules tool
is fixed or carries an inline reason. Complexity (C90, PLR) is left for
group 8b.

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

* Register group 8a in the ruff-lint rule

sim.quality.ruff-lint now lists ARG, ASYNC, B, C4, FBT, PD, PERF, PGH,
PLW, PT, PTH, RUF and SIM.

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

* Sync the rules tool from the Public API's group 8a

The shared scripts/rules tool takes the Public API's group 8a fixes:
comprehensions, explicit check=False, bound loop variables and the
removed unused workflow-parser parameter. The rendered rules page picks
up the tool's new hash.

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

* Fix the ruff group 8a findings in the code #743 and #744 added

#743 and #744 landed on main after this stack branched. Group 8a, which
this PR enables, flags their new code; each fix follows this PR's
conventions:

- F821: this PR's CI-script commit dropped the package updater's os
  import, but #744's verify-dispatch reads os.environ, so restore it.
- PTH123: the routing and sentinel CLIs open their files through Path.
- RUF100: the sentinel's report helper prints with an explicit file, so
  its T201 suppression is unused.
- ARG: tests request serialized_workflow through usefixtures (and the
  patched_modal fixture keeps it with a reason), drop unused monkeypatch
  requests, and prefix unused parameters of positional stand-ins.
- FBT002: the routing-plan test helper takes force by keyword.
- PT012/PT018/B905/RUF043: one call per pytest.raises, one condition per
  assert, an explicit non-strict zip over call chains of different
  lengths, and a raw match pattern.

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

* Fix the ruff group 8a findings in the code #789 added

#789 landed on main after this stack branched. Group 8a, which this PR
enables, flags its new code; each fix follows this PR's conventions:

- ARG001/ARG002: open_connection_once and the telemetry-recording test
  runtime keep keyword parameters their protocols pass, with reasons; the
  test's random.uniform stand-in prefixes its unused positional bound.
- PERF203: the connection retry loop catches each refused attempt by
  design, with a reason.

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

* Fix the ruff group 8a findings in the code #787 added

#787 landed on main after this stack branched. Group 8a, which this PR
enables, flags its new code; each fix follows this PR's conventions:

- ARG002: the shared in-memory object store keeps upload_bytes's
  content_type, which Stage12ArtifactStore passes by keyword, with the
  reason this PR gave the per-file fake it replaced.
- PT011: the invalid SPM provenance test expects pydantic's
  ValidationError, which every rejected value raises, not any ValueError.

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

* Fix the ruff group 8a findings in the code #795 added

#795 landed on main after this stack branched. Group 8a, which this PR
enables, flags its new code; each fix follows this PR's conventions:

- ARG002: the child-deadline tests take the bare_country fixture through
  pytest.mark.usefixtures, like the runner tests.
- ARG005: their _reduce stand-ins keep country_module, which run() passes
  by keyword, with the same reason.
- ARG001: _crashing_call prefixes the clock it takes only to match
  _stuck_call's signature.
- PT006: the child-timeout telemetry test names its parameters in a tuple.
- SIM105: _cancel_call keeps its try/except, whose except line carries the
  BLE001/S110 reason, like the best-effort cancel it replaced.

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

* Fix the ruff group 8a findings in the code #791 added

enables, flags its new tests; each fix follows this PR's conventions:

- ARG002: its fake Modal calls, recording runtimes and rejecting
  aggregate writer keep the timeout, status_code and prefix keywords of
  the interfaces they stand in for, with the reasons this PR gave the
  other fakes.
- PT011: the stage-and-role summary test matches the message it raises
  rather than accepting any ValueError.

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

* Fix the ruff group 8a findings in the code #758 added

#758 landed on main after this stack branched. Group 8a, which this PR
enables, flags its new code; each fix follows this PR's conventions:

- B008: CallerAuthorizer.require_permission builds the authenticate
  dependency marker once and reads it from the nested dependency's
  default. The Annotated form can't be used: the module postpones
  annotations, and FastAPI resolves them against the module's globals,
  which don't hold the authorizer's dependency. FastAPI sees the same
  dependency, so each route's permission check is unchanged.
- SIM105: the permission-missing event keeps its try/except, whose
  except line carries the BLE001/S110 reason, like the module's other
  telemetry.
- ARG: the permission tests' fake decoders explain their unused
  parameters and the _decoder replacements prefix theirs; each
  table-driven test underscores the PROTECTED_ROUTES column it doesn't
  read, so the test ids stay the same.
- ARG001: the deployed enforcement test requests permission_enforcement
  through pytest.mark.usefixtures; pytest still sets it up first, so the
  test skips before fetching a token.

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

* Fix the ruff group 8a findings in the code #781 added

#781 landed on main after this stack branched. Group 8a, which this PR
enables, flags its new code; each fix follows this PR's conventions:

- ARG: stand-ins whose callers pass a parameter positionally prefix it
  with an underscore (the progress store's reads and writes, the patched
  Simulation.ensure, _country_module, get_region, Function.from_name, the
  preempted _set_modal_call_attributes, time.sleep and the
  on_first_ensure stand-ins), and unused *args/**kwargs become
  *_args/**_kwargs. Parameters callers pass by keyword keep a reason: the
  impact-analysis, _load_dataset, dispatch, run_simulation_impl and
  _configure_modal_observability stand-ins, the spawn stand-in's
  observability_context, the _reduce stand-in's country_module, the fake
  FunctionCall.get's timeout and the generated client's sync_detailed.
- ARG001/ARG002: the contract's missing-record test and the bundle
  manifest test request their fixtures through pytest.mark.usefixtures.
- B018: the failed-analysis test assigns the property it reads inside
  pytest.raises.
- SIM105: the Modal app's stage-duration telemetry keeps its try/except,
  whose except line carries the BLE001/S110 reason.

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

* Exempt tests from the unused-argument rules file-wide

Test fakes and stubs mirror real signatures, because callers pass those
arguments by keyword: run() passes country_module=, the runtime passes
keyword-only options, and stand-ins replace modal.FunctionCall.get and
JWTDecoder.__call__. Fixtures also request other fixtures only for their
side effects.

Tests are among the files allowed file-level exceptions, so ARG001,
ARG002 and ARG005 join the tests' and the deployed-service tests'
exemptions. That removes 72 inline suppressions from 16 test files.
Application code keeps its six unused-argument suppressions and their
reasons.

Comments and configuration only: no code changes.

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

* Swallow best-effort errors with contextlib.suppress

Nine blocks ran a best-effort call and swallowed any error with
try/except Exception: pass:
- telemetry context and events in the entry app, its auth, the Modal
  app and the gateway;
- a Modal call cancel during segmented national cleanup.

Each block carried three suppressions: SIM105 on the try, and BLE001
and S110 on the except.

Each is now with contextlib.suppress(Exception), with the old reason as
a comment above it. It suppresses the same exceptions and runs the same
calls, so behaviour is unchanged. That removes 18 suppression lines.

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

* Fix the ruff group 8a findings in the code #712 added

#712's segmentation and cache code raised 16 group 8a findings. Each is
fixed in the code, with no new suppression:

- PERF203: the policy-period parser and the segment-cancellation loop
  move their try/except into a helper called once per item. The parser's
  error helper is now typed NoReturn, since it always raises. The
  cancellation helper keeps #712's BLE001 reason.
- RUF022: stage12_runtime's __all__ is sorted.
- Tests: subprocess.run states check=False (PLW1510), match= patterns
  are raw or escape a literal dot (RUF043), compound asserts are split
  (PT018), the import guard extends its list from a generator (PERF401),
  boolean helper and parametrized arguments are keyword-only
  (FBT001/FBT002), and the segment-function stand-in is an instance
  whose spawn method takes Modal's observability_context by keyword
  (ARG004), as modal.Function.from_name returns an object.

The rebase onto #712 also dropped this branch's PERF203 suppression in
coordination.py, since #712 replaced the loop it was on.

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

* Lint the executor as Python 3.13, its required version

Ruff reads no project's requires-python here, so it linted the executor
with its default target version. PERF203 applies only below Python 3.11,
so the retry loop in database_retry.py needed a suppression that the
executor's own Python (3.13) doesn't call for.

Add the executor to per-file-target-version as py313, remove that
suppression, and fix the eight findings the new target version reports:
tomllib is now sorted with the standard library (I001, five files), and
datetime.timezone.utc becomes datetime.UTC (UP017, three files).

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

* Use or underscore the arguments behind five ARG suppressions

- NoOpObservability now subclasses the Observability protocol it already
  implemented, and span() is marked @OverRide, which ruff exempts from
  ARG002.
- The executor's in-memory object store records each object's content
  type, and the gateway's mock FunctionCall records the timeout each get()
  was called with, so both fakes use the arguments they mirror.
- The gateway's import_gateway_models fixture only requested the module
  isolation fixture for its side effect. The test that used it now requests
  isolation with usefixtures and imports the models module itself, named
  in GatewayImportModuleNames.models.
- The executor's FastAPI lifespan takes _app: Starlette and FastAPI pass
  the app positionally.

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

* Make the precompute entrypoint's force option keyword-only

Modal 1.4.2 builds a command-line option for every non-variadic parameter
of a local entrypoint and calls it with keyword arguments, so a
keyword-only force still gives --force/--no-force. The test already calls
main(force=True). This removes the FBT001/FBT002 suppression.

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

* Replace the SIM115, SIM118 and PT011 suppressions with code

- setup_gcp_credentials enters the credentials temp file on an ExitStack
  held by a with statement around the yield. The file still stays open
  across the yield; the finally block restores GOOGLE_APPLICATION_CREDENTIALS
  first, and the stack then closes, and so deletes, the file, the same order
  as before.
- The UK local-authority check calls HDFStore.keys(include="pandas"), the
  default spelled out, and keeps a comment on why `in store` differs.
- The native SPM test passes check= to pytest.raises (pytest 8.4), so the
  typed error code is part of the raises check instead of a later assert.

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

* Restore the lower-case coverage pragma comment in gateway auth

The contextlib.suppress change rewrote "# pragma: no cover" as the prose
"# Pragma: no cover". Put back the original lower-case spelling. The
comment sits on its own line above the with statement, so coverage.py
excludes no statement because of it either way.

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

---------

Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk added a commit that referenced this pull request Oct 20, 2026
… (ALL) (#804)

* Import test modules with from-imports instead of aliases

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

* Group test helper parameters into composite fixtures and option objects

Split the long deployment-workflow test into focused tests and hoist
closure-free test doubles to module level.

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

* Pin contract and persistence validation behaviour before refactoring

Characterize every branch of the comparison report record's state
validation, the live column type checks, Hugging Face URI parsing, the
SPM non-US and historical-wrapper rules, and the v2 application name
limits.

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

* Split the comparison report record's state validation into report and comparison phases

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

* Name the contract's URI, version and name-length limits

Parse SPM wrapper and model release versions through one helper, and keep
the keyword signatures of the public contract builders that the gateway
and executor call.

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

* Split the live column type check into enum and plain type checks

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

* Keep the observability initializer's keyword-only identity arguments

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

* Pin the order and messages of entry settings validation

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

* Split entry settings validation into caller auth, old gateway and Stage 12 checks

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

* Pin the Stage 12 adapter's complete report input and check order

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

* Split the Stage 12 annual-comparison adapter into eligibility checks and a report builder

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

* Pin the Stage 12 dispatch calls, acknowledged parent and skip event

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

* Group Stage 12 dispatch inputs into a prepared report, origin and worker version

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

* Pin entry app telemetry, Stage 12 route failures and backend readiness

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

* Split the entry app factory into a shared runtime and route registration steps

The routes keep their names, decorators, parameters and registration order,
so the published OpenAPI document is unchanged.

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

* Compare the old gateway's health status with HTTPStatus.OK

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

* Pin gateway route resolution from the active routing state

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

* Split gateway active-state route resolution into pinned, latest and requested-version steps

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

* Pass the resolved region to _build_simulation instead of its code and scoping strategy

Every caller already held a RegionResolution, so the builder now takes it
whole. Also name the simulation runtime's preview, date-range and state-code
limits.

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

* Pass artifact key inputs to the dataset and baseline key builders as dataclasses

The payloads are built from the same fields in the same way; the golden
digest tests still pass unchanged.

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

* Pass baseline identity facts as one request and split out the artifact scope check

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

* Pass the precompute's Modal function handles to run_precompute as one group

The precompute local entrypoint keeps its --force CLI signature.

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

* Pin every input the national reduce passes to the output builder

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

* Pass the national reduce's run facts to build_national_output as one group

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

* Pass fan-out poll timing to the batch and segmented runners as one backoff

Also name the segmented runner's consecutive poll-error limit.

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

* Pin the Stage 12 bundle schema version check

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

* Name the supported Stage 12 bundle schema version

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

* Pin every Stage 12 parity mismatch code, path and comparison order

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

* Split Stage 12 parity comparisons into entity, column and report-value checks

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

* Pin parity qualification receipts, check order and default runner lifecycle

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

* Split parity qualification into existing-run, provenance, calculation and aggregate steps

Group the injectable runners and the reviewed tolerances into two option
objects, and update the qualification script and tests.

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

* Pin Stage 12 coordination validation, claims, failures, stages and aggregate output

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

* Pass each role's retained frames and descriptor to the aggregate builder together

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

* Split Stage 12 report coordination into claim, plan, dispatch, wait and aggregate phases

Group the coordinator's Modal identity and its injectable seams, and split
submitted-parent validation into invocation, claim-state and production
checks. The Modal coordinate_report function keeps its signature.

Rebased onto #712, the phases carry #712's coordinator: one shared
child-wait window opened before any setup, nonblocking probes of both
children, cancellation of the unfinished children on any failure, and
failure details with correlation IDs. CoordinatorDependencies gains
#712's sleep, monotonic and wait_timeout_seconds seams. The report-level
failure handling moves into _ReportCoordination.fail, and the run's child
plans, calls and descriptors live on _ReportCoordination, so a failure in
any phase can stop the started children. The coordination pins expect
#712's single child-wait span and correlation-ID suffixes.

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

* Pin how the Modal child invoker looks up, spawns and restores children

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

* Pass the child invoker one spawn request instead of six keywords

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

* Pin result comparison logs, spans and the failed state-update path

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

* Pass the result comparison its coordinator services as one group

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

* Pin the Stage 12 worker's reuse path and calculator timing

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

* Pass the single-simulation worker its seams as one dependencies group

The Modal run_single_simulation_us and _uk functions keep their signatures.

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

* Pin Stage 12 worker validation results, order and failures

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

* Pass Stage 12 worker validation its environment and checks as one group

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

* Pin routing state validation messages and check order

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

* Group a deployed app's versions in the routing publisher and split its validation

The update_version_registry command-line options are unchanged.

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

* Pin the artifact fetch layer's receipt, credential and download edge cases

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

* Split the image artifact fetch into freshness, credential and download steps

The helpers stay in the self-contained image setup module with lazy imports.
Changing fetch_artifacts's source changes its Modal build-layer definition,
so the next simulation image build re-runs the artifact fetch layer once.

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

* Pin the release sentinel's polling, retry statuses and problem order

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

* Split the release sentinel's deployment check into submit, poll and compare steps

Group the job timeout, poll interval, sleep and clock as one JobPolling,
and name the HTTP statuses the sentinel compares with. The check command's
options are unchanged.

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

* Pin the routing publisher CLI's mode order, outputs and argument errors

Also pin how a publication id names its owning run, and the restore
refusals for no plans and a missing routing store.

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

* Split the routing publisher CLI into one command per mode

The modes keep their precedence and output, and the help text is
unchanged. Also split the plan-restore loop out of restore_routing_plans
and name the parts of a workflow run's publication id.

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

* Pin the package update helper's bundle-versions and verify-dispatch output

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

* Split the package update helper's bundle and dispatch checks into helpers

The helper's subcommands, output lines and messages are unchanged.

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

* Pin the Stage 12 connection retry's delay sequence and budget cut

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

* Pass the Stage 12 connection retry its clock, sleep and jitter as one timing

The persistence store still calls the opener with only the connect step
and the operation name.

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

* Pin the workflow expression evaluator and routing command discovery

Pin what the GitHub expression evaluator returns for each operator and
status function, and the argv and refusal messages routing command
discovery produces, before splitting both test helpers.

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

* Split the workflow expression evaluator into a tokenizer and a parser

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

* Split routing command discovery into script, array and per-command checks

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

* Group the routing and package-update test parameters into cases

Give the fake git its reported state as one FakeGitState, pass a planned
release's versions to _plan together, and parametrize each multi-value
test case as one argument. The parametrized test ids are unchanged.

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

* Take the Public API's group 8b rules tool refactor

Copy the Public API's scripts/rules, whose group 8b commits split the
command line, the registry reader, validation, the workflow job parser and
the mermaid graph builder, and pin their behaviour with new tests. The two
copies were identical apart from that refactor and its tests.

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

* Select every ruff rule (ALL)

Every rule family now passes, so ruff.toml selects ALL instead of the
per-group list. It ignores COM812 and ISC001, which conflict with ruff
format (ruff's documentation says to disable them with the formatter), and
D203 and D213, which are mutually exclusive with the kept D211 and D212.
The per-file target version, namespace packages and per-file ignores are
unchanged.

The sim.quality.ruff-lint rule's enforcer codes become ["ALL"], and its
statement names the four ignores and why. The rules-registry skill says
every rule applies apart from the listed and registered per-file ignores,
that a ruff upgrade PR fixes any new rule's violations, and that an
exception is an inline noqa with a reason. The rendered rules page and the
AGENTS.md rules block are regenerated, and the changelog fragment
describes the complexity refactors and the switch.

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

* Pin the poll, deadline and budget timing the segmented runner #795 extended

#795 landed on main after this stack branched and gave the segmented
national runner a child deadline, a parent budget and a clock beside its
poll backoff. PLR0913, which this PR enables, flags the eight-argument
initializer; these tests pin the deployed defaults and how given values
are taken before the timing is passed as one group.

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

* Pass the segmented runner the poll and deadline timing #795 added as one group

enables, flags the segmented national runner's initializer once #795's
child deadline, parent budget and clock sit beside the poll backoff. The
four now travel as one SegmentedRunTiming with the same defaults, which
the previous commit pins; run_segmented_national keeps its signature.

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

* Set the fake child call's cancel failures #795 added after construction

#795 landed on main after this stack branched. PLR0913 flags the
six-argument initializer of the segmented national tests' fake child call
once #795 added before_each_poll and cancel_failures. No test passes
cancel_failures to the initializer; the two that use it set it on the
call, so it now starts at zero and stays an attribute.

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

* Pin how the tracked_stage #791 added scopes and records each stage

#791 landed on main after this stack branched. PLR0913, which this PR
enables, flags its six-argument tracked_stage, which other tests cover
only through the coordinator and worker. These tests pin it directly
before its stage plan, progress and runtime are passed as one group: the
span or operation it opens, the attributes it passes, the role it records
and the failing stage it keeps without a runtime.

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

* Pass the tracked_stage #791 added its stage plan, progress and runtime as one group

enables, flags its six-argument tracked_stage. Each process's stage plan,
progress and runtime now travel as one StageTracking: the worker builds
one per run, and the coordinator derives its own from the report it
coordinates. The previous commit's pins keep their assertions.

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

* Compare the word count in the redaction helper #791 added without a magic 2

#791 landed on main after this stack branched. PLR2004, which this PR
enables, flags the len(words) == 2 test in its _before_last_word. Splitting
once from the right yields at most two parts, so len(words) > 1 is the
same test.

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

* Parametrize the permission tests #758 added by one route case

#758 landed on main after this stack branched. PLR0913, which this PR
enables, flags its six-argument log-only permission test. As this PR's
other table-driven tests do, each protected route is now one case: the
PROTECTED_ROUTES table holds a ProtectedRoute per route, and the four
tests over it take one route argument, which also drops the underscored
columns two of them didn't read. The parametrized test ids are
unchanged.

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

* Pin the runtime and job progress the segmented runner #781 reports to

#781 landed on main after this stack branched and gave the segmented
national runner a job progress reporter beside its runtime. PLR0913,
which this PR enables, flags the now six-argument initializer; these
tests pin that a runner built without one reports no job progress, and
that run_segmented_national_impl gives its runner the runtime and job
progress it was called with, before the two are passed as one group.

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

* Pass the segmented runner its runtime and the job progress #781 added as one group

#781 landed on main after this stack branched. PLR0913, which this PR
enables, flags the segmented national runner's initializer once #781's
job progress reporter sits beside its runtime. The two now travel as one
SegmentedRunReporting, whose progress defaults to reporting nothing as
before; run_segmented_national_impl and dispatch_run_simulation keep
their signatures, and the previous commit's pins keep their assertions.

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

* Name the numbers #712's segmentation and cache code compares against

#712 landed on main after this stack branched. PLR2004, which this PR
enables, flags five magic values in its code; each is now a named
constant, with no behaviour change:

- the date-range period's two parts (_DATE_RANGE_PART_COUNT, the name
  simulation_runtime already uses);
- the segment poll's consecutive-error limit (CONSECUTIVE_POLL_ERROR_LIMIT,
  as in segmented_national);
- the national partition's 20 region groups
  (NATIONAL_PARTITION_GROUP_COUNT, next to US_REGION_GROUPS);
- worker validation's cache years, artifact count and probe year, from
  the cache precompute's PRECOMPUTE_YEARS: one dataset per year and one
  baseline per US region group per year make the 63 artifacts.

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

* Observe the children the #712 coordinator waits for as a coordination method

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its eleven-argument _observe_simulations. It becomes
_ReportCoordination._observe_children, which takes the calls and
descriptors and reads the child plans, store, runtime, progress, stage
plan, clock, sleep and wait window from the coordination it belongs to.
The polling, failure and timeout logic is unchanged, and #712's runtime
tests cover every path through it.

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

* Pass the failure reports #712 added where each failure was caught as one site

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its seven-argument report_failure. The telemetry scope,
default error code, attributes and segment index that say where a
failure was caught now travel as one FailureSite; the error, runtime and
progress stay separate. The coordinator's three calls, the worker's and
the segment runner's pass the same values as before.

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

* Pass the segment runner #712 added its lookup and polling seams as one group

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags Stage12SegmentRunner's nine-argument constructor. The
Modal module, region groups, sleep and the two poll intervals that tests
replace now travel as one SegmentRunnerSeams, whose defaults are the
deployed ones; the simulation, app name, runtime and cancellation check
stay separate. run_segmented_simulation passes nothing new.

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

* Pass the Stage 12 simulation builder #712 added its resolved inputs as one group

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags its eight-argument build_stage12_simulation. The country
module, region, dataset selection and loaded dataset the worker resolves
in its own stages now travel as one ResolvedSimulationInputs. The builder
reads the scoping strategy from the region, as _build_simulation already
does, so it no longer takes the region code it only deleted. The SPM
parameters, normalised policy and execution input stay separate.

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

* Pass the cache precompute #712 added its Modal function handles as one group

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags the cache precompute's seven-argument run_precompute. Its
five Modal function handles now travel as one CachePrecomputeFunctions,
as the baseline precompute's PrecomputeFunctions already do; the bucket
and echo stay separate. The Modal app's entrypoint passes the same five
functions, and its secrets and environment are unchanged.

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

* Pass the scripted child call #712's tests added its outcome as one script

#712 landed on main after this stack branched. PLR0913, which this PR
enables, flags the six-argument ScriptedChildCall its runtime tests use.
The result, wait error, cancel error and during-wait hook that script a
call's outcome now travel as one ChildCallScript; the call ID and clock
stay separate. Both constructions script the same outcomes as before.

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

* Pin which simulations the Stage 12 cache identity #712 added qualifies

#712 landed on main after this stack branched. Before its eight-return
qualifying_cache_identity is split, pin the ineligible cases its tests
did not reach: no or a non-region-group scoping strategy, a missing,
empty or non-list region group, a non-state or non-text region code, and
a scope without a cache key. Each gets no cache identity.

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

* Split the Stage 12 cache eligibility #712 added into baseline, region and scope checks

PLR0911 flags the eight returns of qualifying_cache_identity. It now
asks three questions: is this a current-law baseline on the default
dataset, what is its group of state regions, and what is the
region-group scope's cache key. Each check is pure, so asking them in
this order gives the same result; the previous commit's pins and
#712's identity tests keep their assertions.

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

* Pin the frame comparison and probe checks of the determinism check #712 added

#712 landed on main after this stack branched. Before its C901
verify_determinism_impl is split, pin what its tests did not reach: every
kind of frame difference (an entity missing on one side, a column on only
one side, a dtype change and a value change) in its sorted order, the
rejection of a probe that is not a planned baseline, and that the stored
baseline is downloaded only when it is not already in the data folder.

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

* Compare the determinism check's frames #712 added in their own function

C901 flags verify_determinism_impl. The comparison of cached and fresh
frames moves into _frame_differences, which lists each difference in the
same sorted order; the probe check, download, cache-hit check and pass
record stay in verify_determinism_impl. The previous commit's pins keep
their assertions.

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

* Pin every check of the segment merge #712 added

#712 landed on main after this stack branched. Before its C901
merge_segment_results is split, pin the checks its tests did not reach:
a result set that omits a segment, a result naming another region group,
segments with different entities or column dtypes, and an entity without
its identifier. The serializer refuses frames without an identifier, so
that pin decodes every segment to one directly.

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

* Split the segment merge #712 added into indexing, decoding and entity merge steps

C901 and PLR0912 flag merge_segment_results. It now indexes the results
(rejecting an incomplete set or a duplicate index), decodes each planned
segment in partition order (rejecting an omitted segment or one naming
another role or region group), and merges each entity's frames
(rejecting different entities, dtypes, a missing identifier or duplicate
IDs), each in its own function. The checks and their order are
unchanged, and the previous commit's pins keep their assertions.

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

* Pin the claim and cancellation-check paths of the worker #712 extended

#712 landed on main after this stack branched and extended
run_single_simulation, which C901 and PLR0915 now flag. Before it is
split, pin the paths its tests did not reach: a simulation that is no
longer active is refused, a claim another writer won stops the run as
cancelled, and a failed status read in the cancellation check is logged
by error type only and treated as not cancelled, so the run completes.

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

* Split the Stage 12 worker #712 extended into claim, calculate and record steps

C901 and PLR0915 flag run_single_simulation, which #712 extended with
segmented calculation and cancellation. It now reads as its phases: claim
the simulation (refusing one that is no longer active or that another
writer claimed), build the cancellation check, write the input,
calculate (segmented, standard or injected, always as a
SimulationCalculation), write the artifact, and record success or
failure, each phase but the writes in its own function. The order of
reads, writes and reports is unchanged, and the previous commit's pins
keep their assertions.

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

* Pin every check of the image-build cache fetch #712 added

#712 landed on main after this stack branched. Before its C901, PLR0912
and PLR0915 fetch_stage12_cache is split, pin what its two tests did not
reach: each manifest rejection (no bucket or manifest, schema, bundle,
year set, artifact list, artifact count and deployment digest), each
artifact rejection (a non-object entry, a filename with a directory, a
duplicate filename, a non-integer size and a same-size file with another
digest, which leaves no partial file), and the storage client built with
no credentials, a credentials object or credentials encoded as a string.

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

* Split the image-build cache fetch #712 added into manifest, client and file steps

C901, PLR0912 and PLR0915 flag fetch_stage12_cache. It now validates the
whole manifest first (schema, installed bundle and years; the artifact
list and its counts; then the deployment digest), builds the storage
client from the deployment's credentials only when none is given, and
fetches each artifact through a partial file. The checks, their order
and the environment variables it reads are unchanged; the module stays
self-contained for the image build. The previous commit's pins keep
their assertions.

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

* Drop init_simulation_observability's never-passed service_version

None of the four callers (the entry app, the gateway app and its test
factory, and the executor's FastAPI app) or the tests passes
service_version, and build_runtime falls back to the package version when
it is absent. Without it the function takes five arguments, so its
PLR0913 suppression goes. Modal workers still pass service_version
through init_process_observability.

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

* Pass create_initial_batch_state its route as one BatchRoute

The batch's reported version and its Modal app name always travel
together: the gateway takes both from its route resolution, and the batch
scheduler from its batch context. create_initial_batch_state now takes
them as one frozen BatchRoute(version, app_name), so it has five
arguments and needs no PLR0913 suppression. The stored batch state is
unchanged. The gateway, the scheduler and the tests pass a BatchRoute.

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

* Pass resolve_spm_selection the route's versions as one SPMRouteVersions

The policyengine.py version, the model version and the route provenance
are read only together, to decide whether a route with no SPM capability
predates canonical SPM. resolve_spm_selection now takes them as one
frozen SPMRouteVersions, so it has four arguments and needs no PLR0913
suppression. Its decisions are unchanged. The gateway, the executor and
the tests pass an SPMRouteVersions.

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

* Call Stage 12 runtime functions through v2_app's module-level import

v2_app.py already imports policyengine_simulation_executor.stage12_runtime
at module level, and that package's __init__ imports its submodules
eagerly, so the four in-function imports of the same package loaded
nothing new and each needed a PLC0415 suppression. The Modal function
bodies now call stage12_runtime.<name> instead. Attribute lookups happen
at call time, so tests that patch stage12_runtime.run_single_simulation
or stage12_runtime.coordinate_report still take effect.

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

* Import the image setup module's standard-library modules at module level

Splitting fetch_artifacts into six functions gave each one its own
in-function imports of json, logging, os and pathlib.Path, raising the
module's PLC0415 suppressions from 7 to 14. Those modules are always
importable in the image-build step: Modal imports this module from the
function's mounted source package (the function is not serialized), and
stage12_cache_image.py, which also runs before add_local_python_source,
already imports json, os and pathlib at module level. The module now
imports them, and typing.TYPE_CHECKING, at module level, which removes
twelve suppressions.

test_image_setup_module_stays_self_contained now allows module-level
imports of standard-library modules only (sys.stdlib_module_names), and
still rejects any import of the executor package.

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

* List google.oauth2 and policyengine_uk as lazily loaded packages

Add both to banned-module-level-imports, so ruff keeps them out of
module-level imports (TID253) and accepts their in-function imports
without a PLC0415 suppression. That removes the two remaining
suppressions in _image_setup.py.

stage12_cache_image.py was the only application module that imported
google.oauth2 at module level; the import moves into _storage_client,
the one function that uses it. The storage-client test now patches
google.oauth2.service_account.Credentials directly, as the image setup
tests already do.

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

* Update the changelog entry for the removed PLR0913 suppressions

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

---------

Co-authored-by: Anthony Volk <14987227+anth-volk@users.noreply.github.com>
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.

Stage 12 coordinator can outlive its timeout while waiting and leaves workers running Segment and cache Stage 12 US simulations

1 participant