Repository navigation
Accept null SPM provenance in Stage 12 aggregate artifacts - #787
Merged
Merged
Conversation
anth-volk
force-pushed
the
fix/stage12-null-spm-provenance
branch
from
October 14, 2026 17:17
f7525ee to
8278e12
Compare
Reports without an SPM calculation, including every UK report, serialize the optional spm_provenance field as null. The aggregate validator checked only that the key existed and then validated null as an SPMComparisonProvenance object, so every such aggregate was rejected before it was saved. Validate the field only when a value is supplied. Malformed provenance records are still rejected. Fixes #786 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Stage 12 artifact and runtime tests each defined the same in-memory object store, and the runtime tests imported the macro result from the API contract tests' fixture module. Move the object store to fixtures/fake_object_store.py and the macro result and its key sets to fixtures/macro_output.py, and import both from there in every test that uses them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FakeArtifacts.write_aggregate encoded whatever it was given, so no
coordinator test enforced AggregateReportArtifactPayload, and the null
SPM provenance rejection reached production without a failing coordinator
test. Write aggregates through the real Stage12ArtifactStore over the
shared in-memory object store instead, so the coordinator tests run the
same validation and encoding as production.
The coordinator tests' stand-in aggregators returned payloads such as
{"result": "complete"} that never satisfied the contract. They now return
a contract-valid payload with the same result. Aggregators that must not
run return one too, so an unexpected run is still caught as a written
aggregate rather than hidden by a validation failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
fix/stage12-null-spm-provenance
branch
from
October 14, 2026 19:50
25c1ee0 to
0951722
Compare
2 tasks done
anth-volk
force-pushed
the
fix/stage12-null-spm-provenance
branch
from
October 14, 2026 19:56
0951722 to
d29ce9d
Compare
anth-volk
marked this pull request as ready for review
October 14, 2026 20:13
anth-volk
added a commit
that referenced
this pull request
Oct 14, 2026
#787 made the coordinator tests' artifact double write aggregates through the real Stage12ArtifactStore, which rejects anything that is not a valid AggregateReportArtifactPayload. Use _aggregate_payload for this PR's aggregator stubs, as #787 did for main's. The redacted comparison-failure test needs a valid aggregate for the report to succeed. An aggregator that must not run now shows up as a written aggregate if it does run, instead of as a rejection hidden by the failure the test expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 14, 2026
#787 made the coordinator tests' artifact double write aggregates through the real Stage12ArtifactStore, which rejects anything that is not a valid AggregateReportArtifactPayload. Use _aggregate_payload for this PR's aggregator stubs that must not run, as #787 did for main's, so an unexpected run shows up as a written aggregate instead of as a rejection hidden by the failure the test expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 tasks done
anth-volk
added a commit
that referenced
this pull request
Oct 14, 2026
#787 made the coordinator tests' artifact double write aggregates through the real Stage12ArtifactStore, which rejects anything that is not a valid AggregateReportArtifactPayload. Use _aggregate_payload for this PR's aggregator stubs that must not run, as #787 did for main's, so an unexpected run shows up as a written aggregate instead of as a rejection hidden by the failure the test expects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 14, 2026
#787 landed on main after this stack branched. It renamed the macro result fixture module to fixtures.macro_output and added fixtures.fake_object_store, and those imports in the output builder, Stage 12 artifact and Stage 12 runtime tests are unsorted under the I rule this PR enables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
added a commit
that referenced
this pull request
Oct 14, 2026
…n class (#791) * Record a redacted Stage 12 failure cause instead of only the exception class Stage 12 coordinator, worker and result-comparison failures stored only type(error).__name__, so a database session limit, a UK aggregate validation defect and an invalid reform path all appeared as RuntimeError or ValidationError behind "Stage 12 report coordination failed". Failure records now store a bounded summary of the form "<type> in|after <stage> (<role>): <message>", capped at the 512-character error_summary limit. Messages are redacted first: URL credentials, signed URL query strings, secret-looking assignments, bearer and known token formats, this process's secret environment values, Pydantic input values and SQL statement parameters are removed. Stage tracking follows the existing registered span names, so the summary names the stage that failed or the last stage that finished before a failure between spans. The coordinator no longer overwrites a cause that the failed worker already persisted, and copies that cause onto the report record. The redacted cause, with the original stack, is also recorded on the active operation before the failure is persisted. The exceptions returned to Modal are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Redact more credential formats in Stage 12 failure causes Redact quoted JSON and dictionary keys, non-Bearer Authorization schemes and cookie headers, signed query strings without a URL scheme, and the row values that PostgreSQL constraint violations echo in their detail, from both the stored summary and the exported cause. Patterns that could start inside a run of word characters now start only where that run starts, so redaction is linear in the message length. Redaction also reads at most 4,096 characters: the word that limit cuts is dropped, and quoted values, SQL parameters and validation inputs whose closing delimiter was cut are redacted to the end. Take the summary length from the contract's ErrorSummary, detect SQLAlchemy errors by type, and state in the module docstring that span exception events are outside this redaction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Share one stage helper for Stage 12 spans and failure progress Replace the coordinator's stage_scope and stage_span closures and the worker's _simulation_stage with tracked_stage, which opens a stage's span or operation and records it as the process's progress. The coordinator claim span uses it too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Test dispatch and comparison failure summaries in Stage 12 Assert the redacted summary stored on a child and its report when Modal rejects a dispatch, and the comparison_error_summary stored when the production result cannot be read. Reword fixture failure messages that described themselves as sensitive, since the tests now assert those messages are stored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Return valid aggregates from the Stage 12 test aggregators #787 made the coordinator tests' artifact double write aggregates through the real Stage12ArtifactStore, which rejects anything that is not a valid AggregateReportArtifactPayload. Use _aggregate_payload for this PR's aggregator stubs, as #787 did for main's. The redacted comparison-failure test needs a valid aggregate for the report to succeed. An aggregator that must not run now shows up as a written aggregate if it does run, instead of as a rejection hidden by the failure the test expects. 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 15, 2026
* Enable ruff group 1 (safe auto-fixes) and fix everything it finds Select FLY, FURB, I, PIE, PYI, RET, RSE, UP and W, and fix every finding: ruff's safe fixes (import sorting, pyupgrade), then by hand: - policyengine-fastapi re-exports JWTDecoder through __all__ and drops two unused imports (the 9 F401s noted in Phase 0). - Two tests build their JSON receipts with json.dumps, not % formatting. - smoke_app uses min() rather than sorted()[0] (ruff 0.16 FURB192). scripts/** is linted as Python 3.11, the shared rules tool's minimum, so import sorting matches policyengine-api byte for byte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Run ruff check in CI and enforce the lint rule The lint job now runs ruff check . on the whole repository, and sim.quality.ruff-lint moves from advisory to enforced with the codes selected in ruff.toml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Add a changelog entry for ruff group 1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sort the imports in the budget output test from #767 #767 landed on main after this stack branched, and its new test imports are unsorted under the I rule this PR enables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sort the imports #743 and #744 added #743 and #744 landed on main after this stack branched. A dataset-digest test from #743 and the tomllib import #744 added to the deployment assets test are unsorted under the I rule this PR enables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sort the imports #787 added #787 landed on main after this stack branched. It renamed the macro result fixture module to fixtures.macro_output and added fixtures.fake_object_store, and those imports in the output builder, Stage 12 artifact and Stage 12 runtime tests are unsorted under the I rule this PR enables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sort the imports #791 added #791 landed on main after this stack branched. I, which this PR enables, flags the import block of its new Stage 12 failure-cause tests; ruff's safe fix sorts it. 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 16, 2026
* Annotate policyengine-fastapi for ruff ANN Add the missing parameter and return annotations. AppExit forwards its callback arguments as object rather than Any, the JWT decoder's private validator returns the same dict type its caller declares, and the metrics middleware types call_next as Starlette's RequestResponseEndpoint. Ping endpoints already pass response_model explicitly, so their new return annotations do not change the generated responses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the simulation contract library for ruff ANN Type the SPM resolution and validation entry points, the gateway request pre-validators (object in, object out) and the Modal Dict store helpers. The three wrap model serializers in spm.py stay unannotated with a reasoned noqa: Pydantic turns a serializer's return annotation into the model's serialization JSON schema, which would rewrite the OpenAPI spec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the observability and Stage 12 persistence libraries for ruff ANN Replace Any with object for values that are only inspected or passed through, and type the persistence row helpers with SQLAlchemy's RowMapping, Table and ColumnElement (imported for type checking only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the simulation gateway for ruff ANN Every annotated route passes response_model explicitly, so FastAPI keeps using it and the OpenAPI golden file is unchanged. _bundle_payload keeps Any for **dump_kwargs, forwarded unchanged to BaseModel.model_dump, with a reasoned noqa. Test fixtures outside tests/ are annotated too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the simulation entry service for ruff ANN Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the simulation executor for ruff ANN Type the runtime, output builders, Modal entrypoints, scripts and the test fixtures that live outside tests/. PolicyEngine, Google Cloud Storage and observability types are imported for type checking only, country modules are ModuleType, and duck-typed analysis values that tests replace with SimpleNamespace are object. _try_compute_output uses a ParamSpec so its arguments are checked against the wrapped function. The in-repo src/modal package shadows the Modal library here, so Modal types stay quoted or behind TYPE_CHECKING. _image_setup.py may not import anything at module level, so it uses a local TYPE_CHECKING flag. The RemoteFunction protocol keeps Any (with a reasoned noqa) because it mirrors modal.Function.remote and spawn, whose payloads vary per function. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Enable ruff group 3 (annotations) Select ANN. Tests are exempt, alongside the docstring, magic-number and assert rules a later group will enable. scripts/rules findings are left for the next sync from policyengine-api. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the package-update script and the Stage 12 proof fixtures _json_from_stdin returns object (every caller narrows it), and the proof fixtures annotate their SQLAlchemy connection and table parameters. The schema-authority proof reads the fixtures as text, so it is unaffected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sync the annotated rules tool from policyengine-api Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Register ANN and the test-file ruff exemptions sim.quality.ruff-lint now includes ANN, and the **/tests/** per-file ignore list (S101, D1, ANN, PLR2004) is registered as sim.quality.tests-ruff-exemptions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the routing and bundle helpers #743 and #744 added #743 and #744 landed on main after this stack branched. ANN, which this PR enables, wants annotations on #744's routing-store, publication-journal and active-state helpers and on #743's bundle dataset-digest helper. The Modal store annotations are quoted, as in the gateway, because the executor tests replace the modal module with a stub. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the shared object store fake #787 added #787 landed on main after this stack branched. ANN, which this PR enables, wants annotations on the in-memory object store it moved into the executor's fixtures. They match the byte interface of the real ArtifactStore it stands in for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the segmented national child calls #795 added The respawn helpers take and return the Modal FunctionCall that Function.spawn returns, rather than Any. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the permission checks #758 added #758 landed on main after this stack branched. ANN, which this PR enables, flags three of its additions: - CallerAuthorizer.__init__ returns None; - the gty before-validator takes the raw claim as object, as the project's other before-validators do, since it narrows the value with isinstance checks; - the deployed permission-enforcement test returns None. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Annotate the job progress code #781 added #781 landed on main after this stack branched. ANN, which this PR enables, flags six of its additions: - the contract's job progress store returns a modal.Dict, as the budget-window stores do; - the Modal app's reporter factory returns a JobProgressReporter, and the stage-duration recorder takes the runtime and that reporter; - StoredJobProgressReporter.__init__ returns None; - the calculation-start hook takes the StageReportingSimulation whose on_first_ensure it calls. The new annotation-only imports sit under TYPE_CHECKING. 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 16, 2026
* Document the simulation contract library for ruff D Validators, state helpers and URI functions get docstrings. Every Pydantic model and enum whose class docstring would become a JSON-schema description carries a D101 noqa instead, so the published contract schemas and the OpenAPI documents built from them stay byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the FastAPI support library for ruff D The ping and health models and the /ping/alive route are mounted in the executor app, so they keep their existing (OpenAPI-visible) docstrings under noqa. The no-op observability methods keep their explicit return None under RET501 noqa, and two stray string statements in the GCP module become the docstrings they were meant to be. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the simulation observability library for ruff D Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the Stage 12 persistence library for ruff D Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the simulation entrypoint for ruff D The entry routes are nested in create_app, so their docstrings (the OpenAPI descriptions) are untouched and the generated spec is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the simulation gateway for ruff D The /ping router endpoint keeps its OpenAPI description under a D205 noqa; the golden OpenAPI test still passes. Two placeholder pass statements in the test fixtures' mock exception classes go now that the classes have docstrings (PIE790). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the simulation executor for ruff D Covers the executor package, the Modal apps, fixtures, scripts and test docstrings. Docstrings are added to several Modal functions; images copy src/modal into a layer, so this changes those layers like any source edit. Protocol stubs keep their ... bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the integration tests, repo scripts and package markers for ruff D Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the Stage 12 schema-DDL proof fixtures for ruff D The schema-authority proof reads the fixtures as text; the new docstrings avoid DDL phrases, so the bad fixture is still rejected and the good one accepted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Enable ruff group 4 (docstrings) Select pydocstyle (D) and ignore D203 and D213, the incompatible halves of the D203/D211 and D212/D213 pairs. Tests stay exempt from D1 only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Drop redundant return None from documented no-op methods With a docstring as the body, the bare return None lines are redundant; remove them instead of suppressing RET501. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document published contract models and routes Replace the 61 D101 suppressions on contract, ping and health models with class docstrings written for API consumers, and fix the /ping/alive and gateway /ping route docstrings instead of suppressing D205. Pydantic and FastAPI copy these into schema and operation descriptions, so the entry, gateway and executor OpenAPI documents gain descriptions; paths, operations, properties, types and required lists are unchanged. The gateway golden spec is regenerated with generate_openapi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Use the Public API's docstrings in the shared rules tool Both repos documented scripts/rules independently for ruff D, so the copies drifted apart. Take the Public API's wording so the tool stays byte-identical in both repos; only docstrings differ. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Register D in the ruff-lint rule sim.quality.ruff-lint now lists D, and the rendered rules page and AGENTS.md Rules block are regenerated for the new tool hash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #767 added in the imperative mood #767 landed on main after this stack branched. D401, which this PR enables, wants each docstring's summary line in the imperative mood. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #743 and #744 added in the D style #743 and #744 landed on main after this stack branched. D, which this PR enables, flags their new code: blank lines after docstrings (D202), a two-line summary (D205, D209), summaries that are not in the imperative mood (D401), an escaped backslash in the sentinel's module docstring (D301), and three public functions without docstrings (D103). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #789 added in the D style #789 landed on main after this stack branched. D, which this PR enables, flags blank lines after its new docstrings (D202) and the undocumented __call__ of its ConnectionOpener protocol (D102). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Document the shared object store fake #787 added #787 landed on main after this stack branched. D, which this PR enables, flags the undocumented __init__ (D107), upload_bytes and read_bytes (D102) of the in-memory object store it moved into the executor's fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #795 added in the D style D, which this PR enables, flags the undocumented group_label property (D102), the budget check's run-on summary (D205, D209) and two test helpers' summaries that are not in the imperative mood (D401). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #791 added in the D style #791 landed on main after this stack branched. D, which this PR enables, flags blank lines after five of its new docstrings (D202) and the undocumented __init__ (D107) and stage method (D102) of its StageProgress tracker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #758 added in the D style #758 landed on main after this stack branched. D, which this PR enables, flags its permission checks and deployed permission test: - CallerAuthorizer's initializer and authorize now say what enforcement does with a caller that lacks a permission, and that the missing permission is always recorded; - the permission_enforcement fixture says when it skips and when it raises, and the enforced-permission deployment test says what it checks; - the blank lines after the docstrings of is_client_credentials, granted_permissions, the gty validator and the Cloud Run deployment-environment test helper go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Put the docstrings #781 added in the D style #781 landed on main after this stack branched. D, which this PR enables, flags its job progress code: - the JobProgressReporter protocol and each of its methods say which stage boundary the runtime reports; the null reporter's methods say what they discard, with the docstring as the body; - StoredJobProgressReporter's initializer, record, start, set_execution, enter, start_segments, segment_completed and attempts say how the record changes, including when a call changes nothing; - the contract's job progress store functions say what they read and write, and StageReportingSimulation and its ensure say when the callback runs; - the floor and maximum helpers, the Modal reporter factory, the published-record helper and the gateway's stage-field helper are in the imperative mood, and blank lines after docstrings go. 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #786
Same-Repository Draft PR Check
PolicyEngine/policyengine-sim-api, not a personal fork.Summary
Stage 12 rejected every aggregate report without an SPM calculation, which includes every UK report. Between 6 and 13 October 2026 this caused 42 of the 153 production Stage 12 failures. All 42 were UK nationwide reports whose baseline and reform simulations had already succeeded.
SingleYearMacroOutput.spm_provenanceis optional and defaults toNone, so a report with no SPM calculation serializes"spm_provenance": null.AggregateReportArtifactPayload.validate_spm_provenancechecked only that the key was present. It then validatednullas anSPMComparisonProvenanceobject, which failed insideStage12ArtifactStore.write_aggregate. The coordinator recorded the failure as the genericStage 12 report coordination failed.spm_provenanceonly when a value is supplied. An absent or null field is accepted. Every non-null value is still validated, so malformed or mismatched provenance records are still rejected.FakeArtifacts.write_aggregate, saved any payload without validating it. It now writes through the realStage12ArtifactStore, so the coordinator tests enforce the same aggregate contract as production.stage12-deploy.ymlrun and needs no change to the selected worker version. The other changes are test-only.Testing
mainafter Release Stage 12 database sessions and keep original failures visible #789 and Add a rules registry that makes every enforced and unenforced rule visible #752. The three coordinator tests Release Stage 12 database sessions and keep original failures visible #789 added now use_aggregate_payloadtoo, because one of them aggregates successfully and the real store rejects{"result": "complete"}.libs/policyengine-simulation-contract/tests/test_stage12_execution.py:spm_provenanceand a null one are both accepted.{}, a string, a list,0) are rejected.projects/policyengine-simulation-executor/tests/test_stage12_runtime.py:build_aggregate_report, using the realSingleYearMacroOutputserialization.FakeArtifacts.write_aggregate, which now calls the realStage12ArtifactStore.write_aggregateover an in-memory object store.spm_provenance: nulland matches the returned digest.FakeArtifacts.write_aggregatedelegates to a realStage12ArtifactStore, so it runs the sameAggregateReportArtifactPayloadvalidation and encoding as production. The stored aggregate path now follows the production layout under the run's artifact prefix, and one coordinator test checks that path.{"result": "complete"}. They now return a valid payload from a_aggregate_payloadhelper, with the sameresultas before.aggregate_writes == []checks would fail, rather than a validation error hiding the run.docs/engineering/skills/testing.md:fixtures/fake_object_store.pyholdsInMemoryObjectStore. It replaces the two identical copies intest_stage12_artifacts.pyandtest_stage12_runtime.py.fixtures/macro_output.py, renamed fromfixtures/test_simulation_api_contracts.py, holds the shared macro result and its key sets. The Stage 12 runtime, output builder and standalone app tests no longer import the API contract tests' fixture module.ValidationError. In a one-off check with the previous validator, a coordinator run whose aggregate carriesspm_provenance: nullalso failed through the validating double, recordingValidationError. The absent-field and invalid-value cases pass both before and after, as guards should.uv run --frozen:libs/policyengine-simulation-contract: 153 passed.projects/policyengine-simulation-executor, all oftests/: 1058 passed, 22 skipped, 3 deselected (theintegrationmarker).tests/test_stage12_runtime.pyandtests/test_stage12_artifacts.py: 54 passed.ruff format --check,ruff checkandgit diff --checkpass on every changed file.🤖 Generated with Claude Code