Skip to content

Preserve canonical SPM settings and receipts across API calculations - #3827

Merged
MaxGhenis merged 23 commits into
masterfrom
max/canonical-spm-api-20260909
Sep 11, 2026
Merged

Preserve canonical SPM settings and receipts across API calculations#3827
MaxGhenis merged 23 commits into
masterfrom
max/canonical-spm-api-20260909

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Canonical SPM calculations need reproducible measurement inputs and saved results. This adds typed SPM settings and receipts to household calculations, saved simulations, annual economy requests, and budget windows. Selections participate in household identity and cache keys. Typed worker failures retain their original terminal code and message across polling and service recreation, and strict numeric household aliases preserve historical simulation/report spelling and output.

Native v2 household storage and reads retain saved US SPM selections in household_data.spm and its canonical content hash without resolving new bundle defaults. Historical documents without SPM retain their shape and identity. Stage 11 immutable household creation, durable events, migration selectors, and persistence failures remain intact; changing inputs, labels, or selections creates a replacement household. Calculation execution remains on the existing Flask/API v1 and worker paths. Query validation intentionally rejects unsupported fields and invalid boolean spellings. There is no new DDL. The worker handoff is in PR677; protective images remain separate in PR3824.

This integrates current API 3.54.2 master aeaa3de1b50382ccbb322c56e81ba4b44dde752c and preserves its disposable MySQL/PostgreSQL/Redis CI and removal of staging database exercises. The workflow retains both canonical MySQL suites with regression guards and matching contributor commands. The lock refresh changes only the root API version; all resolved dependency pins remain unchanged. API runtime source equals the previously reviewed canonical source at 83ec1710b540f55849deee865ad6a64dc605023b.

Validation for head 50d1f0c95da1206c17fc59ce1fc2f345bbfe3e5e:

  • Resolved-merge unit/contract coverage: 2,010 passed, 37 native capability-dependent skips, using the repository's CI collection order. The final workflow/documentation correction also passed 169 focused tests, zero skips.
  • Fresh disposable databases: 123 executions passed, zero failures or skips. Each actual MySQL collation (utf8mb4_0900_ai_ci and utf8mb4_unicode_ci) passed eight MySQL/PostgreSQL lifecycle tests, 23 policy persistence/mirroring tests, and 20 household persistence/SPM identity tests. Another 16 PostgreSQL catalog-fixture/route tests and five real Redis tests passed. Actual servers were MySQL 8.4.11, PostgreSQL 17.11, and Redis 7.2.16. Source and installed package version/RECORD metadata stayed unchanged; every owned container was removed afterward.
  • Formatting, changed-Python Ruff checks, configured mypy (83 files), migration guards/export (13 workflows and 50 requests), and whitespace checks passed. Independent merge review found no blocking defect; independent review of the final two-file correction found no actionable issues.

Local checks used Python 3.14.4 and the pinned legacy runtime: PolicyEngine 5.2.0, Core 3.30.1, US 1.764.6, UK 2.90.2, and SPM calculator 0.3.1. The 37 skipped cases require a coherent canonical runtime. Historical projected scientific-source checks do not qualify that final dependency graph or the eventual published package tuple. The real database suites qualify persistence behavior; they do not certify final population results, full installed-catalog publication, a deployed worker, managed cache, or production serving.

Keep this unmerged pending required CI, eligible GitHub review, new-head Fable agreement, and the separately gated release qualification using the actual published runtime tuple. No source review or local test result is a production deployment receipt.

@codecov

codecov Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.33189% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.04%. Comparing base (770ed7d) to head (908d5df).
⚠️ Report is 24 commits behind head on master.

Files with missing lines Patch % Lines
policyengine_api/worker_spm.py 81.70% 7 Missing and 8 partials ⚠️
policyengine_api/libs/simulation_entrypoint.py 78.00% 5 Missing and 6 partials ⚠️
policyengine_api/routes/household_routes.py 88.00% 4 Missing and 5 partials ⚠️
policyengine_api/spm.py 94.91% 5 Missing and 4 partials ⚠️
policyengine_api/specification.py 82.22% 1 Missing and 7 partials ⚠️
policyengine_api/services/economy_service.py 87.50% 4 Missing and 2 partials ⚠️
policyengine_api/country.py 77.77% 1 Missing and 1 partial ⚠️
...engine_api/runtime_cache/household_calculations.py 89.47% 1 Missing and 1 partial ⚠️
policyengine_api/services/household_service.py 81.81% 1 Missing and 1 partial ⚠️
policyengine_api/query_parameters.py 97.56% 0 Missing and 1 partial ⚠️
... and 2 more
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3827       +/-   ##
===========================================
+ Coverage   49.20%   88.04%   +38.83%     
===========================================
  Files         161      179       +18     
  Lines        9227    10853     +1626     
  Branches     1538     1917      +379     
===========================================
+ Hits         4540     9555     +5015     
+ Misses       4318      772     -3546     
- Partials      369      526      +157     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

MaxGhenis and others added 7 commits September 11, 2026 11:45
Brings in PR #3824 protective dependency pins. The only uv.lock change is
master's spm-calculator==0.3.1 declaration; the reviewed runtime tuple
(policyengine 5.2.0, core 3.30.1, US 1.764.6, UK 2.90.2, spm-calculator 0.3.1)
is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A resolved selection freezes all six fields, but a canonical country's receipt
may omit its null geography_id/as_of, which the worker path already allows. The
household trace cache required exact JSON equality, so every stored replay of a
canonical calculation missed the cache and recomputed. Both paths now share one
rule in spm.resolved_spm_settings, which still refuses to infer an omitted
non-null setting from today's bundle defaults.

A receipt this API cannot read in full is an uncertified country contract, so
calculation_spm_receipt now raises a typed SPM_CONFIGURATION_UNAVAILABLE instead
of letting pydantic turn a completed calculation into a 500. The receipt is still
published with the country's own omissions and never with fields dropped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sion

An allowlist of wrapper versions made every uncertified bundle reject every US
request, including stored replay, household creation and economy submission. The
automated bundle update moves those strings on its own, so a release for another
country would have taken the US surface down rather than stalling only SPM.

The boundary now asks the installed country model whether it implements the
canonical constructor. A model without it computes SPM as it always has and still
refuses explicit settings; a model with it still fails closed until the bundle
ships a certified configuration. That remaining fail-closed case now also fails
the readiness probe, so it is reported where the deployment is gated instead of
only on each request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Certifying a bundle must not reject callers who never asked for a measurement.
A measurement is now chosen by a request that sends spm, or by the household
whose saved selection a replay reads; omitting spm still inherits the certified
defaults to construct the simulation but chooses nothing. A chosen measurement
whose primitives are missing stays a typed 400. An inherited one leaves its
dependants null, the way any other variable the model cannot compute behaves,
and the rest of the calculation returns normally. A configuration failure is
never nulled: only a missing primitive can make a dependant unavailable.

POST /us/household stores a selection only when the caller sent one.
Certification still runs, so an unservable bundle is still rejected, but writing
back the defaults it resolved fabricated a choice nobody made: it moved the
household hash and the GET shape for identical inputs, and it made the
household's own replay assert a measurement its creator never asked for. An
explicit "spm": null is now rejected rather than read as an omission, matching
the v2 document validator and the simulation routes.

A saved artifact hash this deployment does not have is a configuration failure,
not a caller error; the household's selection is its identity, and the same
mismatch sent in a request is still SPM_SETTINGS_INVALID. SPM messages no longer
quote pydantic's report, which carried its version-pinned documentation URL and
echoed the offending input.

An uncertifiable stored economy result now records a terminal error on both the
annual and budget-window paths. Both stores already replay a recorded typed
failure before reading their success payload, so without recording one the same
payload was re-validated into the same 400 on every poll, with no terminal state
to reach and the worker handle never released. Recording is best effort and only
against the request's own record: a prefix-matched record belongs to another
options-hash spelling.

Simulation creation no longer takes a locking read on the linked household row.
Households are immutable, so the lock serialized against a mutation path that
does not exist, while a locking read of an absent numeric id takes an InnoDB gap
lock that briefly blocks unrelated household inserts into that range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The commitment that an omitted selection never turns a missing primitive into a
400 does not extend to a bundle this build cannot serve at all, which fails
every US request by design and is meant to stop a deployment at the readiness
probe. Saying otherwise would promise behaviour the code does not implement.

Also drop a claim about a receipt's empty years and geographies: that shape
comes from the country package's provenance, which this API reads rather than
guarantees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s in agreement

The previous round changed what an omitted SPM selection does without changing
everything that already described the old behavior.

Two database-backed tests only CI runs still asserted that a household created
without `spm` stores the resolved defaults, so the required cross-database and
Alembic v1 jobs would have failed on push. They now send a selection where they
mean to carry one, and a new case pins the commitment: a creation that sends
none persists and mirrors without one, keeping its hash.

Seven skipped real-runtime tests encoded the chosen-measurement behavior while
calling `calculate` without saying the measurement was chosen. They would have
inverted the day the canonical model made them run. Each now says which case it
is, and new companions assert the other half against the real model and over
HTTP: no selection, HTTP 200, null dependants.

A bundle manifest, a worker capability and a worker receipt are validated by the
same models a request is, and all three still published pydantic's report --
documentation URL and echoed input -- into a caller's 400 body, which
`docs/canonical-spm.md` now said never happens. One rule renders them all.

A country package installed without its `Simulation` raised AttributeError out
of the certified branch. `is_ready` recovers from RuntimeError and ValueError
only, so `/readiness-check` raised instead of reporting not-ready, and every US
request became a 500 rather than the typed 400 the document promises.

An `spm` key on a country that has no SPM settings is unsupported, as
`POST /{country}/simulation` already answered; only US null is a shape to
correct. The capability probe resolves its package through the repository's own
country map rather than a formula that is wrong for Canada.

The document claimed an unreadable country receipt fails `/readiness-check`;
readiness resolves settings and never constructs a simulation. It claimed every
US request fails, while `/us/metadata` answers. It promised a structured
`SPM_YEAR_UNAVAILABLE` the inherited-default rule now returns as null, and a
create event that always carries a selection. The served specification still
described the storage and calculate behavior this contract replaced.

Also restores commit 56af1cf: the rollout journal stays outside the API source
change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MaxGhenis and others added 3 commits September 11, 2026 13:57
…ract

Master removed household tracing, rewrote the axes branch of the country
adapter and replaced the household-trace cache family with a calculated-household
one. Re-express the canonical SPM behaviour inside that structure rather than
resurrecting the deleted module: the receipt-settings comparison moves to
HouseholdCalculationCache, the chosen-versus-inherited guard sits in front of
the new per-variable failure recorder, and the receipt is spread onto master's
CalculationResult beside its warnings.

An axes request now spells an unavailable cell as a correctly sized array of
nulls beside a response warning, so the inherited-measurement contract and its
tests say that rather than "the seeded null survives". The cache family's schema
version moves to 2 because its payload and identity changed after master shipped
version 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The receipt-settings comparison moved onto master's calculated-household cache
family, so the real-Redis qualification should read one back across two
connections, including a receipt that omits its null settings, and miss on a
different measurement. Say so in the testing skill as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… SPM contract

Master now routes a stored failed execution to its own handler and answers it
HTTP 502 with the upstream message. Move the typed SPM replay into that handler
so a poll-time configuration failure still reaches the caller with its code
rather than being flattened into an untyped upstream failure, and say 502 in the
failure-replay test: an untyped failure is the simulation service's, not the
caller's.

Read any failure to load a country package as a model without the canonical
constructor. The probe's narrower guard let an extension that will not
initialize escape as an internal error on a bundle whose behaviour is simply
the legacy one, and it made the economy service suite fail whenever that import
first ran under a patched datetime.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Master built its changelog and moved the version. Nothing in this branch touches
either, so the merge is a fast-forward of master's files onto the contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review September 11, 2026 18:56
@MaxGhenis
MaxGhenis merged commit 2694a56 into master Sep 11, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant