Preserve canonical SPM settings and receipts across API calculations - #3827
Merged
Conversation
Codecov Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
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>
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.spmand 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
aeaa3de1b50382ccbb322c56e81ba4b44dde752cand 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 at83ec1710b540f55849deee865ad6a64dc605023b.Validation for head
50d1f0c95da1206c17fc59ce1fc2f345bbfe3e5e:utf8mb4_0900_ai_ciandutf8mb4_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.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.