diff --git a/CLAUDE.md b/CLAUDE.md index 2857eb1f..2303d9a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -80,6 +80,13 @@ build recorded staging telemetry that never reached its repo publishes without the flag. Never publish or promote artifacts as a side effect of another task. +A US release or release-gate preflight that receives a multispine pool through +`--base-h5` must authenticate its sibling terminal manifest. A current stacked +pool whose terminal battery is red remains fail-closed unless the operator +passes `--allow-gate-failed-base-pool`; that opt-in carries the full red verdict +into `release_manifest.json` for a separate human publication decision. It does +not weaken the exact-k manifest arm or authorize publication by itself. + ## Root journals are history, not state The root `PROGRESS*.md`, `FINAL_REPORT.md`, `*_COVERAGE_PROGRESS.md`, and diff --git a/PROGRESS.md b/PROGRESS.md index 3036d509..a0394ddf 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1,63 +1,89 @@ -# Progress: stacked-pool to release CD-vintage provenance +# Gate-failed base-pool release lane ## State -Implementation and validation are complete. The stacked -producer now authenticates and applies household geography after source -assembly, carries its authority through checkpoint and publication identities, -publishes verified CD-vintage H5 attributes, and reaches the unchanged release -guard through the shared fixed/table-aware reader -(`tools/build_us_multispine_pool.py:814-928,1300-1469,1857-1930,5290-5334`; -`packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py:1032-1193,1463-1699`; -`tools/build_us_fiscal_refresh_release.py:2565-2677`). - -No pool/release build, push, guard weakening, operator-boundary weakening, or -`logbook-pending-chain.txt` access has occurred. +Complete on 2026-08-26. Containment, opt-in carriage, and preflight surfacing +are implemented and fully verified. The implementation rejects both redundant +green-pool waivers and any release-manifest receipt that does not exactly match +the pool authenticated by preflight; the preflight's historically required +base/selection inputs and exit semantics are unchanged. No pool or release was +built, no artifact was published, and nothing was pushed. ## Done -- Reconciled the worktree with salvage commit `ca26ea21`: its tracked tree was - already exactly present, so the best salvage was retained and audited rather - than reimplemented. -- Completed the required all-package US environment sync from the exact lock - using the writable offline uv cache after the managed sandbox refused the - default cache. -- Added the two required pinned geography authority pairs, the ledgered seeded - PUMA-overlap assignment, schema-12 checkpoint identity, assignment receipts, - schema-9 terminal manifest validation, and schema-3 H5 materializer binding. -- Added atomic nullable-H5 root-attribute write/verification and authenticated - manifest-to-H5 geography/clone-lineage validation. -- Made release preflight read root attributes and fixed household frames in one - `HDFStore` handle while retaining all existing SHA, target-vintage, and - positive-support checks. -- Added a real tiny stacked-pool publication to release-preflight integration - test plus negative attr, digest, missing-lineage, and divergent-clone tests - (`packages/microcosm-build/tests/test_us_multispine_pool_tool.py:2279-2335`; - `packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py:1357-1440`). -- Regenerated and checked the US anti-rot chain: spec SHA - `5378bb9189aec96f50da22aac71e5bd2c3d919e9795f6ef2147e0bc9c739dd8e`, - 42,120/42,120 configuration fields, 49 claims, and 41/41 inventory checks. -- Passed the complete accepted workspace suite: 7,241 passed, 77 skipped, and - 0 failed. The two memory-bounded build partitions were 4,155 passed / 36 - skipped and 2,177 passed / 3 skipped; calibrate, data, fit, and frame account - for the remaining 909 passed / 38 skipped. -- Passed repository-wide Ruff, bundle freshness, coverage freshness, smoke - script syntax, and `git diff --check`. -- Recorded the exact candidate Stage-1 path/SHA additions and the checkpoint - invalidation verdict in `_LANE-NOTES.md` and `FINAL_REPORT.md`. +- Confirmed the assigned branch and worktree. +- Recorded the v2 charter: close the legacy bare-H5 multispine bypass, add an + explicit release-build opt-in, carry the authenticated red verdict, and + surface it in publication preflight without making it an automatic + publication failure. +- Confirmed that no network, artifact builds, publishing, or pushes are in + scope. +- Traced the strict manifest loader, current stacked-only terminal-failure + exception, H5 identity stamp, pool sidecar naming, legacy release arm, + release manifests, and both preflight output modes. +- Reviewed the salvage branch's final source and test diff line by line. Its + shared classifier/path-binding/receipt approach closes the bypass without + changing either loader's contract and was retained with the subsequent + coherence corrections recorded below. +- Completed the `simulation_ready` / `gate_failed` / loader consumer audit. + Exact-k remains deliberately strict and head-to-head scoring remains the + existing authenticated evidence exception. +- Identified report-only downstream caveats: stacked producer metadata still + names only the k-ladder readiness consumer; red pool publication returns + status 1 and stops shell chains; ACS-local derivatives keep a donor revision + but do not project the nested red verdict; generic release consumers tolerate + and ignore the additive receipt. +- Added a release/preflight-specific authenticated pool loader over the shared + `require_simulation_ready` seam. The strict simulation-ready and existing + scoring-only loader contracts remain unchanged. +- Closed the bare-H5 path by detecting either the canonical sibling manifest + or the H5's stamped pool identity, requiring the sidecar, authenticating the + publication triple, and binding it to the exact requested H5 path. +- Added `--allow-gate-failed-base-pool` only to the legacy `--base-h5` arm. + It admits only a current authenticated stacked `gate_failed` pool, rejects a + green or non-pool use, and never affects the exact-k arm. +- Added the self-contained `base_pool` receipt to both manifests, including + status/readiness, immutable pool identities, flag use, gates JSON SHA-256, + failure count/list, and the complete terminal verdict. +- Kept the static preflight inputs mandatory, authenticated its base identically, + displayed red evidence prominently without changing its exit calculation, + and required optional release-manifest carriage to match the authenticated + receipt exactly. +- Hardened carried verdict normalization so nested pass/failure pairs and the + aggregate verdict must be coherent and a red battery cannot report zero + failures. +- Passed focused Ruff and the complete builder/H5/preflight test files after + the containment changes. +- Passed the broader exact-k, launcher, release-contract, and publish-guard + regression suites. +- Passed repository-wide Ruff and the CI test-group inventory verifier. +- Passed every pytest shard in its own process: build 6,545 passed / 45 + skipped; calibrate 203 passed; data 318 passed / 2 skipped; fit 93 passed; + frame 295 passed / 36 skipped. Aggregate: 7,454 passed, 83 skipped. +- Confirmed `git diff --check` is clean and that no battery bounds, + tolerances, plans, or terminal gate logic changed. +- Wrote the complete handoff, consumer audit, manifest schema, verification + receipts, and commit inventory to `out.md`. ## Next -1. Candidate Stage 1 adds both authenticated geography authority pairs and - rebuilds under checkpoint materializer 12. -2. Candidate Stage 2 consumes the new schema-9 manifest/materializer-3 H5 and - passes the unchanged release preflight. -3. Do not reuse pre-fix Stage-1 checkpoints or pool publications; immutable - source artifacts remain reusable. +- Human review and merge of `release-from-gate-failed-pool`. +- Any later artifact operation remains separate: an operator must deliberately + choose the red-pool flag, then run publication preflight and make the human + publication decision. This lane performed none of those operations. ## Historical prior lane -The PolicyEngine-US 1.819.0 lock-bump journal previously in this file is -historical: that lane merged into `origin/main` at `7b90bb18` on 2026-08-24. -Its final state remains available at commit `05d254aa` and its detailed -receipts remain in the historical section of `_LANE-NOTES.md`. +The stacked-pool-to-release CD-vintage provenance lane previously maintained +this journal and completed before this work began. It authenticated and +applied household geography after source assembly, carried that authority +through checkpoint and publication identities, published verified CD-vintage +H5 attributes, and reached the unchanged release guard through the shared +fixed/table-aware reader. Its final verification was 7,241 passed, 77 skipped, +with repository-wide Ruff and anti-rot checks green. Full details remain at +commit `2263df36` (the parent of this lane's first journal commit). + +The still-earlier PolicyEngine-US 1.819.0 lock-bump lane merged into +`origin/main` at `7b90bb18` on 2026-08-24; its final state remains at commit +`05d254aa` and its detailed receipts remain in the historical section of +`_LANE-NOTES.md`. diff --git a/changelog.d/gate-failed-pool-release.fixed.md b/changelog.d/gate-failed-pool-release.fixed.md new file mode 100644 index 00000000..bda525e2 --- /dev/null +++ b/changelog.d/gate-failed-pool-release.fixed.md @@ -0,0 +1 @@ +Close the US fiscal release and release-gate preflight bypass that treated a bare multispine ``--base-h5`` as a generic H5 without authenticating its terminal manifest. ``--allow-gate-failed-base-pool`` now explicitly admits only an authenticated current stacked ``gate_failed`` pool, carries its full red battery receipt under ``build.base_pool``, and surfaces that evidence prominently for a separate human publication decision without changing other preflight exits. diff --git a/out.md b/out.md new file mode 100644 index 00000000..df65be15 --- /dev/null +++ b/out.md @@ -0,0 +1,233 @@ +# Gate-failed base-pool release lane: final report + +Date: 2026-08-26 + +Branch: `release-from-gate-failed-pool` + +Base: `origin/main` at `2263df36` + +## Outcome + +The legacy `--base-h5` release and preflight paths now fail closed when the H5 +identifies as a US multispine pool. They require and authenticate the canonical +sibling pool manifest, bind it to the exact requested H5, and reject a red +terminal agreement battery by default. The new explicit opt-in is: + +`--allow-gate-failed-base-pool` + +The flag is valid only with `--base-h5` and only for an authenticated current +stacked pool with `status=gate_failed` and `simulation_ready=false`. It is +rejected for generic H5 inputs, green pools, and the exact-k manifest arm. A +release built with the flag carries the full authenticated red verdict so a +reader can see `battery: red, N failures` without fetching the pool. + +Publication preflight authenticates the same pool and displays the red receipt +as prominent human-review evidence. The red battery alone does not change the +existing preflight PASS/AT_RISK/FAIL calculation or exit code. Publication +remains a separate human-gated operation. + +No network access, pool build, release build, publication, push, battery +threshold change, or gate-logic change was performed. + +## Diff summary and rationale + +- `CLAUDE.md`: documents the authenticated `--base-h5` boundary, explicit red + opt-in, verdict carriage, strict exact-k arm, and separate publication step. +- `PROGRESS.md`: maintains the required state/done/next journal from kickoff + through the final verified handoff while preserving the prior lane history. +- `changelog.d/gate-failed-pool-release.fixed.md`: records the closed receipt + bypass and explicit opt-in under the repository changelog convention. +- `packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py`: adds the + shared pool classifier, exact-H5 binding check, release-specific authenticated + loader, and normalized self-contained release receipt. The existing strict + simulation-ready and scoring loader contracts were not weakened. +- `packages/microcosm-build/src/microcosm/build/us_runtime/release_gate_preflight.py`: + authenticates identified base pools through the shared seam, records their + receipt in `PreflightReport`, and renders authenticated red evidence before + the ordinary check table without including it in exit-code calculation. +- `tools/build_us_fiscal_refresh_release.py`: adds + `--allow-gate-failed-base-pool`, authenticates pool-like legacy base H5s before + the generic loader can run, uses the authenticated frame/H5 identity, and + passes the receipt into both generated manifests. The exact-k arm remains + simulation-ready-only. +- `tools/preflight_us_release_gates.py`: adds the same explicit opt-in and an + optional `--release-manifest`; validates the carried full verdict, requires + its receipt to exactly equal the pool authenticated from `--base-h5`, emits a + large red/human-review banner, and adds machine-readable carried evidence. + Existing required `--base-h5` and `--selection-source-manifest` arguments and + all other preflight behavior remain intact. +- `packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py`: exercises + real stamped H5 classification, strict/default refusal, explicit red + acceptance, redundant-green refusal, receipt contents, identity binding, and + malformed aggregate/nested verdict refusal. PyTables-dependent cases use the + sibling-test `importorskip` idiom. +- `packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py`: covers the + old bare-H5 bypass directly, red-manifest default refusal, flag routing, + generic/non-pool refusal when the flag is present, both-manifest carriage, + exact-k isolation, and AST-level ordinary base-pool wiring guards. +- `packages/microcosm-build/tests/test_us_release_gate_preflight.py`: covers + bare/missing/red receipt authentication, prominent text and JSON carriage, + nonblocking PASS/AT_RISK/FAIL exit semantics, required static inputs, and + exact mismatch refusal across manifest/H5/publication/gates/full-verdict + identities. +- `out.md`: this final implementation, audit, verification, and handoff report. + +## Flag and manifest contract + +The exact flag is `--allow-gate-failed-base-pool`. The release builder permits +it only on its legacy `--base-h5` arm. Publication preflight uses the same flag +to authenticate and inspect the same red pool; neither invocation is an +authorization to publish. + +The build manifest stores the receipt at top-level `base_pool`. The release +manifest stores the same receipt at `build.base_pool`: + +- `artifact_kind` +- `status` +- `simulation_ready` +- `manifest_sha256` +- `publication_run_id` +- `pool_h5_sha256` +- `pool_h5_size_bytes` +- `allow_gate_failed_base_pool` +- `agreement_gate_reference.battery_status` +- `agreement_gate_reference.passed` +- `agreement_gate_reference.gates_json_sha256` +- `agreement_gate_reference.failure_count` +- `agreement_gate_reference.failures`, a flattened list of `{gate, message}` +- `agreement_gate_reference.verdict`, the complete agreement-gate verdict + +The receipt accepts only coherent pairs: simulation-ready/green/passed or +gate-failed/red/failed. Every nested gate must carry a boolean `passed` and a +coherent failure list; the nested aggregate must match the terminal status. +For a red pool the failure list must be nonempty. SHA-256, publication run ID, +H5 size, full verdict, and all other receipt fields are bound through exact +preflight equality. + +The preflight report also exposes the authenticated receipt as `base_pool`. +When `--release-manifest` supplies a red carried receipt, JSON adds +`carried_base_pool_agreement_battery`, including `battery_status=red`, the +failure count/list, gates digest, full agreement reference, +`publication_decision=human_review_required`, and `affects_exit_code=false`. + +## §4 consumer audit + +The requested exhaustive source audit used +`simulation_ready|gate_failed|load_simulation_ready` across `tools/` and +`packages/`, followed by source tracing of every non-test match. + +- `h5_io.py` owns the authentication boundary. The new release wrapper selects + the unchanged strict loader by default and the existing private terminal + loader only when its required explicit boolean is true. The public scoring + and strict simulation-ready contracts remain unchanged. +- `tools/build_us_exact_k_ladder_release.py` remains deliberately strict: it + uses the simulation-ready loader, validates config/release identities, checks + `agreement_gate.passed`, and never forwards the new flag. +- `tools/score_us_release_head_to_head.py` retains its existing authenticated + terminal-evidence exception. It requires a pool manifest and terminal gates; + it cannot authorize a release or accept a naked pool H5. +- `tools/build_us_multispine_pool.py` and + `us_runtime/multispine_pool.py` are producers of the status, readiness, H5 + stamp, manifest, diagnostics, and terminal verdict rather than downstream + release consumers. +- `tools/_legacy/build_us_acs_multispine_base.py` writes its own pre-calibration + `simulation_ready=false` state; it is not a current terminal-pool consumer. +- `tools/build_us_acs_local_release.py` uses its own calibrated-release + readiness summary. A derivative keeps donor release/revision provenance but + does not project the nested `build.base_pool` verdict into its own manifest. +- The microcosm-data release contract, loader, TRACE conversion, and publisher + tolerate but otherwise ignore the additive `build.base_pool` object. The + designated publication preflight is therefore the prominent human-facing + red-verdict surface. + +Report-only interactions left unchanged, as directed: + +1. `tools/build_us_multispine_pool.py::_stacked_manifest_payload` still labels + `calibration.consumer` as `k-ladder` and says + `requires_manifest_simulation_ready=true`. That metadata is now incomplete + for the explicit legacy base-H5 red-pool route, but it is informational and + unenforced. +2. A red pool producer writes its H5, manifest, gates evidence, and failed + Logbook row, then returns status 1. Existing `set -e` candidate chains stop + there, so an operator must deliberately start the separate release command + to use the new opt-in. +3. ACS-local derivative manifests retain fetchable donor provenance but do not + self-contain the nested red receipt. +4. Generic data/release consumers accept the additive receipt but do not + surface it. This is not a publication bypass because the publication tool's + designated preflight now authenticates and displays it. + +No audit-only consumer was modified. + +## Judgment calls + +- Pool detection uses either a canonical sibling manifest whose artifact kind + is the pool kind or the H5's own artifact-metadata stamp. Either positive + identity requires the sidecar; sidecar existence alone is never trusted. +- A dedicated release loader with a required + `allow_terminal_gate_failure` argument keeps the strict and scoring APIs + semantically stable. +- The opt-in is rejected for a green pool rather than silently accepted, so + every recorded flag use has one unambiguous meaning. +- Preflight requires exact equality between the release-carried receipt and the + freshly authenticated base receipt. This prevents displaying release A's red + evidence while checking release B's pool. +- The earlier salvage branch was used as a reference and selected commits were + replayed only after line-by-line review. The final tree corrected its + manifest-only preflight expansion, loader naming/contract ambiguity, + redundant-green behavior, receipt binding, nested-verdict coherence, and an + inaccurate exact-k diagnostic. Full verification ran on the corrected tree. + +## Verification evidence + +All commands ran offline against the pre-built environment with +`uv run --no-sync` (and a task-local UV cache where required). Each pytest +shard ran in one independent process. + +- `uv run --no-sync ruff check .`: PASS, `All checks passed!` +- `uv run --no-sync pytest -q packages/microcosm-build/tests`: PASS, 6,545 + passed, 45 skipped, 2,351 warnings in 3,326.74 seconds. +- `uv run --no-sync pytest -q packages/microcosm-calibrate/tests`: PASS, 203 + passed, 2 warnings in 11.39 seconds. +- `uv run --no-sync pytest -q packages/microcosm-data/tests`: PASS, 318 passed, + 2 skipped in 9.11 seconds. +- `uv run --no-sync pytest -q packages/microcosm-fit/tests`: PASS, 93 passed, + 1 warning in 30.27 seconds. +- `uv run --no-sync pytest -q packages/microcosm-frame/tests`: PASS, 295 + passed, 36 skipped, 1 warning in 89.30 seconds. +- Full aggregate: 7,454 passed, 83 skipped. +- `.venv/bin/python tools/ci_test_groups.py --verify`: PASS, + `tracked_test_files=309`, `verification=ok`. +- Focused changed-file suites plus exact-k E2E/launcher and data + contract/release/publish-guard regressions: PASS; the full shard runs above + subsequently covered the same tests on the final code tree. +- `git diff --check origin/main...HEAD`: PASS. + +The build-shard warnings are expected numerical, pandas chained-assignment, +PolicyEngine division, and intentionally fragmented-frame test warnings; no +warning is a test failure and none originates in the new receipt path. + +## Commit inventory + +Implementation and journal commits before this final report carrier: + +1. `34375fe6` Document gate-failed release lane kickoff +2. `5be9e49a` Preserve prior progress journal history +3. `c5a24c0f` Record release containment design review +4. `72274910` feat: allow explicit gate-failed pool release inputs +5. `e3d847d6` feat: surface carried red pool verdict in preflight +6. `46811fdb` fix: preserve default exact-k receipt fixtures +7. `dd1ad19a` Add shared multispine base-pool authentication seam +8. `271ee1fe` Close legacy base-H5 pool receipt bypass +9. `92134322` Authenticate and surface gate-failed pools in preflight +10. `cd39f756` Document gate-failed base-pool release boundary +11. `9db4694a` Tighten the authenticated red-pool release opt-in +12. `72c4c7a1` Bind carried pool verdicts to preflight inputs +13. `59c5759d` Record completed release containment work +14. `af20d3f5` Lock ordinary base-pool release wiring in tests + +Commits after the list carry this report, the final `PROGRESS.md` state, and +their handoff cleanup. A report cannot embed the hash of the commit containing +its own final bytes, so `git log --oneline origin/main..HEAD` is the +authoritative complete inventory. At final handoff the branch is 17 commits +ahead of `origin/main` with no uncommitted paths. diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py b/packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py index c63fac0b..516dead3 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/h5_io.py @@ -58,11 +58,15 @@ "US_MULTISPINE_POOL_MANIFEST_ARTIFACT_KIND", "US_MULTISPINE_POOL_MANIFEST_SCHEMA_VERSION", "US_STACKED_POOL_OPERATOR_ORDER", + "identify_us_multispine_pool_manifest", + "load_authenticated_us_multispine_pool_for_release", "load_authenticated_us_multispine_pool_for_scoring", "load_legacy_calibrated_us_h5", "load_simulation_ready_us_multispine_pool", "load_simulation_ready_us_multispine_pool_manifest", "read_nullable_us_h5_metadata", + "require_authenticated_us_multispine_pool_h5", + "us_multispine_pool_release_receipt", "write_nullable_us_h5", ] @@ -328,6 +332,193 @@ def _raise_mismatch( ) +def identify_us_multispine_pool_manifest(path: str | Path) -> Path | None: + """Return the required sidecar when an H5 positively identifies as a pool. + + A successful pool publication writes ``.manifest.json`` beside the + H5 and stamps the H5's artifact-metadata row. Either identity is enough to + require the manifest. The caller must still authenticate that manifest; + this classifier deliberately does not turn sidecar existence into trust. + """ + + h5_path = Path(path) + manifest_path = h5_path.with_suffix(".manifest.json") + manifest_identifies_pool = False + if manifest_path.is_file(): + try: + sibling_manifest = _read_json_object( + manifest_path, + label="candidate US multispine pool manifest", + ) + except (OSError, ValueError): + sibling_manifest = None + manifest_identifies_pool = ( + isinstance(sibling_manifest, Mapping) + and sibling_manifest.get("artifact_kind") + == US_MULTISPINE_POOL_MANIFEST_ARTIFACT_KIND + ) + + # A generic PolicyEngine H5 has no Microcosm artifact-metadata table. Open + # the key inventory first so an unreadable non-pool fixture stays on the + # generic path, while a present-but-malformed identity row remains a hard + # error instead of becoming a receipt bypass. + try: + with pd.HDFStore(h5_path, mode="r") as store: + has_artifact_metadata = _METADATA_KEY in { + key.lstrip("/") for key in store.keys() + } + except Exception: + has_artifact_metadata = False + + h5_identifies_pool = False + if has_artifact_metadata: + metadata = read_nullable_us_h5_metadata(h5_path) + h5_identifies_pool = ( + metadata.get("artifact_kind") == US_MULTISPINE_POOL_H5_ARTIFACT_KIND + ) + if manifest_identifies_pool or h5_identifies_pool: + return manifest_path + return None + + +def require_authenticated_us_multispine_pool_h5( + requested_path: str | Path, + authenticated_pool_h5: AuthenticatedPoolH5, + *, + consumer: str, +) -> Path: + """Require one sidecar to authorize the exact H5 supplied by a consumer.""" + + requested = Path(requested_path).resolve() + authenticated = authenticated_pool_h5.path.resolve() + if authenticated != requested: + raise ValueError( + f"{consumer} supplied US multispine pool H5 {requested}, but its " + f"sibling manifest authenticates a different H5 {authenticated}." + ) + return authenticated + + +def us_multispine_pool_release_receipt( + manifest: Mapping[str, object], + authenticated_pool_h5: AuthenticatedPoolH5, + *, + allow_gate_failed_base_pool: bool, +) -> dict[str, object]: + """Build self-contained release evidence from an authenticated pool.""" + + status = manifest.get("status") + simulation_ready = manifest.get("simulation_ready") + is_ready = status == "simulation_ready" and simulation_ready is True + is_gate_failed = status == "gate_failed" and simulation_ready is False + if not is_ready and not is_gate_failed: + raise ValueError( + "Authenticated US multispine pool has an unsupported release status " + f"pair: status={status!r}, simulation_ready={simulation_ready!r}." + ) + if is_gate_failed and not allow_gate_failed_base_pool: + raise ValueError( + "Authenticated gate-failed US multispine pool requires the explicit " + "--allow-gate-failed-base-pool opt-in." + ) + if is_ready and allow_gate_failed_base_pool: + raise ValueError( + "--allow-gate-failed-base-pool was set, but the authenticated US " + "multispine pool is simulation-ready; the override is valid only " + "for status=gate_failed and simulation_ready=false." + ) + + agreement_gate = _mapping( + manifest.get("agreement_gate"), + label="authenticated US multispine pool agreement_gate", + ) + expected_passed = is_ready + if agreement_gate.get("passed") is not expected_passed: + raise ValueError( + "Authenticated US multispine pool status disagrees with its " + "agreement-gate verdict." + ) + gates = _mapping( + agreement_gate.get("gates"), + label="authenticated US multispine pool agreement_gate.gates", + ) + if not gates: + raise ValueError( + "Authenticated US multispine pool agreement gate has no nested " + "gate verdicts." + ) + failures: list[dict[str, str]] = [] + nested_passed: list[bool] = [] + for gate_name, gate_payload in gates.items(): + if not isinstance(gate_name, str) or not gate_name: + raise ValueError( + "Authenticated US multispine pool agreement gate has an invalid " + "gate name." + ) + gate = _mapping( + gate_payload, + label=f"authenticated US multispine pool gate {gate_name!r}", + ) + gate_passed = gate.get("passed") + gate_failures = gate.get("failures") + if type(gate_passed) is not bool or not isinstance( + gate_failures, list + ) or not all( + isinstance(failure, str) for failure in gate_failures + ): + raise ValueError( + f"Authenticated US multispine pool gate {gate_name!r} has an " + "invalid passed verdict or failure list." + ) + if gate_passed is bool(gate_failures): + raise ValueError( + f"Authenticated US multispine pool gate {gate_name!r} has an " + "incoherent passed verdict and failure list." + ) + nested_passed.append(gate_passed) + failures.extend( + {"gate": gate_name, "message": failure} for failure in gate_failures + ) + if all(nested_passed) is not expected_passed: + raise ValueError( + "Authenticated US multispine pool aggregate agreement verdict " + "disagrees with its nested gate verdicts." + ) + + diagnostics = _mapping( + manifest.get("agreement_diagnostics"), + label="authenticated US multispine pool agreement_diagnostics", + ) + gates_json_sha256 = diagnostics.get("sha256") + if ( + not isinstance(gates_json_sha256, str) + or _LOWERCASE_SHA256.fullmatch(gates_json_sha256) is None + ): + raise ValueError( + "Authenticated US multispine pool agreement diagnostics have no " + "valid SHA-256." + ) + + return { + "artifact_kind": US_MULTISPINE_POOL_H5_ARTIFACT_KIND, + "status": status, + "simulation_ready": simulation_ready, + "manifest_sha256": authenticated_pool_h5.manifest_sha256, + "publication_run_id": authenticated_pool_h5.publication_run_id, + "pool_h5_sha256": authenticated_pool_h5.sha256, + "pool_h5_size_bytes": authenticated_pool_h5.size_bytes, + "allow_gate_failed_base_pool": bool(allow_gate_failed_base_pool), + "agreement_gate_reference": { + "battery_status": "green" if is_ready else "red", + "passed": expected_passed, + "gates_json_sha256": gates_json_sha256, + "failure_count": len(failures), + "failures": failures, + "verdict": dict(agreement_gate), + }, + } + + def load_legacy_calibrated_us_h5(path: str | Path) -> Frame: """Load a legacy US single-year H5 as a calibrated-weight frame. @@ -1285,6 +1476,33 @@ def load_authenticated_us_multispine_pool_for_scoring( ) +def load_authenticated_us_multispine_pool_for_release( + path: str | Path, + *, + allow_terminal_gate_failure: bool, + expected_manifest_sha256: str | None = None, +) -> tuple[Frame, dict[str, object], AuthenticatedPoolH5]: + """Load an authenticated pool for release build or release preflight. + + The default release boundary remains the public simulation-ready loader. + An explicit caller opt-in may instead admit the same current stacked + ``gate_failed`` status pair accepted for evidence scoring. Both branches + authenticate the complete manifest/H5/diagnostics publication; neither + changes the contract of the strict or scoring-only public loaders. + """ + + if not allow_terminal_gate_failure: + return load_simulation_ready_us_multispine_pool( + path, + expected_manifest_sha256=expected_manifest_sha256, + ) + return _load_us_multispine_pool( + path, + expected_manifest_sha256=expected_manifest_sha256, + require_simulation_ready=False, + ) + + def _load_us_multispine_pool( path: str | Path, *, diff --git a/packages/microcosm-build/src/microcosm/build/us_runtime/release_gate_preflight.py b/packages/microcosm-build/src/microcosm/build/us_runtime/release_gate_preflight.py index 2c203fa4..4978ea94 100644 --- a/packages/microcosm-build/src/microcosm/build/us_runtime/release_gate_preflight.py +++ b/packages/microcosm-build/src/microcosm/build/us_runtime/release_gate_preflight.py @@ -64,6 +64,12 @@ import pandas as pd from microcosm.build.gates import input_mass_parity_gate +from microcosm.build.us_runtime.h5_io import ( + identify_us_multispine_pool_manifest, + load_authenticated_us_multispine_pool_for_release, + require_authenticated_us_multispine_pool_h5, + us_multispine_pool_release_receipt, +) from microcosm.build.us_runtime.input_mass import us_input_mass_totals from microcosm.build.us_runtime.puf_capital_gains_tail import ( assert_puf_capital_gains_tail_survives_selection, @@ -159,6 +165,7 @@ class PreflightReport: checks: tuple[CheckResult, ...] inputs: Mapping[str, Any] = field(default_factory=dict) + base_pool: Mapping[str, Any] | None = None @property def status(self) -> PreflightStatus: @@ -179,16 +186,46 @@ def exit_code(self) -> int: return 0 def to_dict(self) -> dict[str, Any]: - return { + payload = { "status": self.status, "exit_code": self.exit_code, "inputs": dict(self.inputs), "checks": [check.to_dict() for check in self.checks], } + if self.base_pool is not None: + payload["base_pool"] = dict(self.base_pool) + return payload def human_table(self) -> str: """A compact, terminal-friendly rendering of every check.""" lines: list[str] = [] + if self.base_pool is not None: + agreement = self.base_pool.get("agreement_gate_reference") + if isinstance(agreement, Mapping) and agreement.get( + "battery_status" + ) == "red": + failure_count = agreement.get("failure_count") + lines.extend( + [ + "!" * 72, + "BASE POOL BATTERY: RED — " + f"{failure_count} FAILURES", + "Authenticated opt-in: --allow-gate-failed-base-pool", + "Gates JSON SHA-256: " + f"{agreement.get('gates_json_sha256')}", + "Human publication decision required; this evidence " + "does not determine the preflight exit code.", + ] + ) + failures = agreement.get("failures") + if isinstance(failures, list): + for failure in failures: + if isinstance(failure, Mapping): + lines.append( + f" [{failure.get('gate')}] " + f"{failure.get('message')}" + ) + lines.append("!" * 72) badge = { "PASS": "PASS ", "FAIL": "FAIL ", @@ -808,6 +845,7 @@ def run_preflight( probes: Iterable[ReformCoverageProbe] | None = None, engine_input_variables: Sequence[str] | None = None, export_mass_reviewed_exclusions: Mapping[str, str] | None = None, + allow_gate_failed_base_pool: bool = False, ) -> PreflightReport: """Load the real artifacts and run every preflight check (no solve). @@ -818,7 +856,10 @@ def run_preflight( from microcosm.build.us_runtime.l0_refit_export import load_us_frame base_h5 = Path(base_h5) - base_frame = load_us_frame(base_h5) + base_frame, base_pool, base_pool_manifest = _load_preflight_base( + base_h5, + allow_gate_failed_base_pool=allow_gate_failed_base_pool, + ) selection_source = load_selection_source_from_manifest(selection_source_manifest) checks: list[CheckResult] = [] @@ -908,6 +949,10 @@ def run_preflight( inputs = { "base_h5": str(base_h5), + "base_pool_manifest": ( + str(base_pool_manifest) if base_pool_manifest is not None else None + ), + "allow_gate_failed_base_pool": bool(allow_gate_failed_base_pool), "selection_source_manifest": str(selection_source_manifest), "export_input_mass_reference_h5": ( str(export_input_mass_reference_h5) @@ -919,7 +964,48 @@ def run_preflight( "relative_tolerance": float(relative_tolerance), "minimum_reference_total": float(minimum_reference_total), } - return PreflightReport(checks=tuple(checks), inputs=inputs) + return PreflightReport( + checks=tuple(checks), + inputs=inputs, + base_pool=base_pool, + ) + + +def _load_preflight_base( + base_h5: Path, + *, + allow_gate_failed_base_pool: bool, +) -> tuple[Frame, dict[str, object] | None, Path | None]: + """Load a generic base or authenticate a positively identified pool.""" + + from microcosm.build.us_runtime.l0_refit_export import load_us_frame + + manifest_path = identify_us_multispine_pool_manifest(base_h5) + if manifest_path is None: + if allow_gate_failed_base_pool: + raise ValueError( + "--allow-gate-failed-base-pool was set, but --base-h5 does not " + "identify as a US multispine pool." + ) + return load_us_frame(base_h5), None, None + + frame, manifest, authenticated_pool_h5 = ( + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=allow_gate_failed_base_pool, + ) + ) + require_authenticated_us_multispine_pool_h5( + base_h5, + authenticated_pool_h5, + consumer="US release-gate preflight --base-h5", + ) + receipt = us_multispine_pool_release_receipt( + manifest, + authenticated_pool_h5, + allow_gate_failed_base_pool=allow_gate_failed_base_pool, + ) + return frame, receipt, manifest_path def _household_person_mask(base_frame: Frame, household_mask: np.ndarray) -> np.ndarray: diff --git a/packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py b/packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py index 48486968..3c76b462 100644 --- a/packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py +++ b/packages/microcosm-build/tests/test_us_fiscal_refresh_builder.py @@ -1547,6 +1547,31 @@ def test_builder_exact_k_parser_enforces_charter_and_explicit_seed(capsys) -> No assert parsed.no_staging is True +def test_builder_gate_failed_base_pool_override_is_explicit(capsys) -> None: + builder = _load_builder_module() + + base_argv = [ + "--base-h5", + "pool.h5", + "--ledger-facts", + "facts.jsonl", + "--out", + "release", + "--no-staging", + ] + parsed = builder._parse_args(base_argv) + assert parsed.allow_gate_failed_base_pool is False + + parsed = builder._parse_args([*base_argv, "--allow-gate-failed-base-pool"]) + assert parsed.allow_gate_failed_base_pool is True + + with pytest.raises(SystemExit): + builder._parse_args( + [*_exact_k_builder_argv("20000"), "--allow-gate-failed-base-pool"] + ) + assert "requires --base-h5" in capsys.readouterr().err + + def test_builder_exact_k_requires_pointer_suppression(capsys) -> None: builder = _load_builder_module() argv = _exact_k_builder_argv("20000") @@ -1643,6 +1668,189 @@ def fake_load_pool(path, *, expected_manifest_sha256): assert not out.exists() +@pytest.mark.parametrize( + ( + "allow_gate_failed", + "status", + "simulation_ready", + "passed", + "rejected", + ), + ( + (False, "simulation_ready", True, True, False), + (True, "gate_failed", False, False, False), + (True, "simulation_ready", True, True, True), + ), +) +def test_builder_base_h5_pool_loader_receives_explicit_terminal_gate_policy( + monkeypatch, + tmp_path, + allow_gate_failed, + status, + simulation_ready, + passed, + rejected, +) -> None: + builder = _load_builder_module() + pool_h5 = tmp_path / "pool.h5" + pool_h5.write_bytes(b"authenticated pool") + manifest_path = pool_h5.with_suffix(".manifest.json") + gate_failures = [] if passed else ["fixture battery failure"] + manifest = { + "status": status, + "simulation_ready": simulation_ready, + "agreement_gate": { + "passed": passed, + "gates": { + "us_by_origin_battery": { + "passed": passed, + "failures": gate_failures, + } + }, + }, + "agreement_diagnostics": {"sha256": "2" * 64}, + } + authenticated = builder.AuthenticatedPoolH5( + path=pool_h5.resolve(), + sha256=hashlib.sha256(pool_h5.read_bytes()).hexdigest(), + size_bytes=pool_h5.stat().st_size, + publication_run_id="fixture-publication", + manifest_sha256="a" * 64, + ) + frame = SimpleNamespace() + monkeypatch.setattr( + builder, + "identify_us_multispine_pool_manifest", + lambda path: manifest_path, + ) + + def selected(path, *, allow_terminal_gate_failure): + assert path == manifest_path + assert allow_terminal_gate_failure is allow_gate_failed + return frame, manifest, authenticated + + monkeypatch.setattr( + builder, + "load_authenticated_us_multispine_pool_for_release", + selected, + ) + + if rejected: + with pytest.raises(ValueError, match="override is valid only"): + builder._load_base_pool_if_identified( + pool_h5, + allow_gate_failed_base_pool=allow_gate_failed, + ) + return + + loaded_frame, receipt, loaded_identity = ( + builder._load_base_pool_if_identified( + pool_h5, + allow_gate_failed_base_pool=allow_gate_failed, + ) + ) + + assert loaded_frame is frame + assert loaded_identity is authenticated + assert receipt["status"] == status + assert receipt["allow_gate_failed_base_pool"] is allow_gate_failed + assert receipt["agreement_gate_reference"]["failure_count"] == len( + gate_failures + ) + + +def test_builder_refuses_actual_red_base_h5_pool_sidecar_without_opt_in( + monkeypatch, tmp_path +) -> None: + builder = _load_builder_module() + pool_h5 = tmp_path / "pool.h5" + pool_h5.write_bytes(b"pool identity comes from its sibling manifest") + manifest_path = pool_h5.with_suffix(".manifest.json") + manifest_path.write_text( + json.dumps( + { + "artifact_kind": "populace_us_multispine_pool_manifest", + "status": "gate_failed", + "simulation_ready": False, + } + ) + ) + out = tmp_path / "out" + monkeypatch.setattr(builder, "_git_dirty", lambda: False) + monkeypatch.setattr(builder, "_refuse_certified_release_dir_reuse", lambda path: None) + monkeypatch.setattr( + builder, + "_load_frame", + lambda path: pytest.fail("red pool reached the generic H5 loader"), + ) + + with pytest.raises(ValueError, match="not simulation-ready"): + builder.main( + [ + "--base-h5", + str(pool_h5), + "--ledger-facts", + str(tmp_path / "facts.jsonl"), + "--out", + str(out), + "--release-id", + "populace-us-2024-red-pool-fixture", + "--no-staging", + ] + ) + + assert not out.exists() + + +def test_builder_refuses_bare_stamped_pool_h5_before_generic_load( + monkeypatch, tmp_path +) -> None: + pytest.importorskip("tables", exc_type=ModuleNotFoundError) + builder = _load_builder_module() + base_h5 = tmp_path / "pool.h5" + with pd.HDFStore(base_h5, mode="w") as store: + store.put( + "_populace_staging_metadata", + pd.Series( + [ + json.dumps( + { + "artifact_kind": "populace_us_multispine_input_pool", + "household_weight_kind": "importance", + "publication_run_id": "fixture-publication", + } + ) + ] + ), + format="table", + ) + out = tmp_path / "out" + monkeypatch.setattr(builder, "_git_dirty", lambda: False) + monkeypatch.setattr(builder, "_refuse_certified_release_dir_reuse", lambda path: None) + monkeypatch.setattr( + builder, + "_load_frame", + lambda path: pytest.fail("bare pool reached the generic H5 loader"), + ) + + with pytest.raises(ValueError, match="pool.manifest.json is not readable"): + builder.main( + [ + "--base-h5", + str(base_h5), + "--ledger-facts", + str(tmp_path / "facts.jsonl"), + "--out", + str(out), + "--release-id", + "populace-us-2024-bare-pool-fixture", + "--no-staging", + ] + ) + + assert not out.exists() + + def test_authenticated_pool_h5_consumers_use_one_returned_identity() -> None: import ast import inspect @@ -1669,10 +1877,41 @@ def test_authenticated_pool_h5_consumers_use_one_returned_identity() -> None: builder._write_release_calibration_diagnostics ) receipt_source = inspect.getsource(builder._exact_k_ladder_manifest_payload) + main_tree = ast.parse(main_source) + base_pool_assignments = [ + node + for node in ast.walk(main_tree) + if isinstance(node, ast.Assign) + and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) + and node.value.func.id == "_load_base_pool_if_identified" + ] + assert len(base_pool_assignments) == 1 + assert [ + element.id + for element in base_pool_assignments[0].targets[0].elts + if isinstance(element, ast.Name) + ] == ["pool_frame", "base_pool_receipt", "authenticated_pool_h5"] + manifest_calls = [ + node + for node in ast.walk(main_tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "_build_manifests" + ] + assert len(manifest_calls) == 1 + base_pool_keyword = next( + keyword for keyword in manifest_calls[0].keywords if keyword.arg == "base_pool" + ) + assert isinstance(base_pool_keyword.value, ast.Name) + assert base_pool_keyword.value.id == "base_pool_receipt" assert ( "authenticated_pool_h5.verified_digest(\n" ' consumer="builder base dataset identity"' ) in main_source + assert "if pool_frame is None:\n base_frame = _load_frame(base_h5)" in ( + main_source + ) assert "base_dataset_sha256=base_dataset_sha256" in main_source assert '"base_dataset_sha256": base_dataset_sha256' in diagnostics_source assert '"manifest_sha256": authenticated_pool_h5.manifest_sha256' in receipt_source @@ -8717,6 +8956,163 @@ def __len__(self): ) +def _gate_failed_exact_k_inputs(builder): + class FakeFrame: + @staticmethod + def n(entity): + assert entity == "household" + return 20_000 + + outcome = SimpleNamespace( + result=SimpleNamespace(frame=FakeFrame()), + selection_receipt={ + "k": 20_000, + "pi_hi": 0.95, + "seed": 17, + "certainty_count": 3, + "boundary_pool_size": 100, + "design": "sampford", + }, + refit_baseline_diagnostics={"method": "fixture"}, + ) + args = SimpleNamespace( + exact_k=20_000, + seed=17, + pool_release_id="fixture-publication", + allow_gate_failed_base_pool=True, + ) + failures = ["asec incidence differs", "puf_tax_detail incidence differs"] + pool_manifest = { + "status": "gate_failed", + "simulation_ready": False, + "agreement_diagnostics": {"sha256": "2" * 64}, + "agreement_gate": { + "passed": False, + "gates": { + "us_by_origin_battery": { + "passed": False, + "failures": failures, + "details": {"fixture": True}, + } + }, + }, + } + authenticated_pool_h5 = builder.AuthenticatedPoolH5( + path=Path("pool.h5"), + sha256="1" * 64, + size_bytes=123, + publication_run_id="fixture-publication", + manifest_sha256="a" * 64, + ) + return { + "args": args, + "outcome": outcome, + "pool_manifest": pool_manifest, + "authenticated_pool_h5": authenticated_pool_h5, + "ledger_artifact": {"facts_sha256": "3" * 64}, + "target_surface": {"sha256": "4" * 64}, + "target_loss_basis": {"method": "fixture"}, + "incumbent_diagnostics_sha256": "5" * 64, + "incumbent_fit_gate": builder.GateResult("incumbent_fit", True), + "puf_tail_gate": builder.GateResult("puf_tail", True), + } + + +def test_exact_k_receipt_stays_strict_even_when_base_h5_opt_in_is_present() -> None: + builder = _load_builder_module() + + with pytest.raises(RuntimeError, match="lost its passing agreement gate"): + builder._exact_k_ladder_manifest_payload( + **_gate_failed_exact_k_inputs(builder) + ) + + +def _gate_failed_base_pool_receipt() -> dict[str, object]: + failures = [ + {"gate": "us_by_origin_battery", "message": "asec incidence differs"}, + { + "gate": "us_by_origin_battery", + "message": "puf_tax_detail incidence differs", + }, + ] + verdict = { + "passed": False, + "gates": { + "us_by_origin_battery": { + "passed": False, + "failures": [entry["message"] for entry in failures], + "details": {"fixture": True}, + } + }, + } + return { + "artifact_kind": "populace_us_multispine_input_pool", + "status": "gate_failed", + "simulation_ready": False, + "manifest_sha256": "a" * 64, + "publication_run_id": "fixture-publication", + "pool_h5_sha256": "1" * 64, + "pool_h5_size_bytes": 123, + "allow_gate_failed_base_pool": True, + "agreement_gate_reference": { + "battery_status": "red", + "passed": False, + "gates_json_sha256": "2" * 64, + "failure_count": len(failures), + "failures": failures, + "verdict": verdict, + }, + } + + +def test_gate_failed_base_pool_verdict_is_carried_into_release_manifest( + monkeypatch, tmp_path +) -> None: + builder = _load_builder_module() + receipt = _gate_failed_base_pool_receipt() + release_id = "populace-us-2024-gate-failed-fixture" + release_dir = tmp_path / "release" / release_id + release_dir.mkdir(parents=True) + artifact_root = tmp_path / "artifacts" + artifact_root.mkdir() + (artifact_root / builder.DATASET_FILENAME).write_bytes(b"h5") + (artifact_root / builder.CALIBRATION_FILENAME).write_bytes(b"npz") + (release_dir / "calibration_diagnostics.json").write_text("{}") + (release_dir / "us_source_coverage.json").write_text("{}") + (release_dir / "us_ssi_take_up.json").write_text("{}") + monkeypatch.setattr( + builder, + "_runtime_versions", + lambda: { + "python": "3.14.0", + "microcosm-data": "0.1.0", + "policyengine-core": "3.26.11", + "policyengine-us": "1.752.2", + }, + ) + monkeypatch.setattr(builder, "_git_output", lambda *args: "a" * 40) + monkeypatch.setattr( + builder, + "diagnostics_payload", + lambda result, target_registry: { + "initial_loss": 2.0, + "final_loss": 1.0, + "fraction_within_10pct": 1.0, + "target_surface": {"sha256": "b" * 64, "n_targets": 1}, + }, + ) + + builder._build_manifests( + base_pool=receipt, + **_minimal_manifest_kwargs(builder, release_id, release_dir, artifact_root), + ) + + build_manifest = json.loads((release_dir / "build_manifest.json").read_text()) + release_manifest = json.loads((release_dir / "release_manifest.json").read_text()) + assert build_manifest["base_pool"] == receipt + assert release_manifest["build"]["base_pool"] == receipt + + def test_build_manifests_uses_loadable_paths_and_round_trips_exact_count_receipt( monkeypatch, tmp_path ) -> None: diff --git a/packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py b/packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py index 54a7fe18..5544e92a 100644 --- a/packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py +++ b/packages/microcosm-build/tests/test_us_multispine_pool_h5_io.py @@ -26,15 +26,21 @@ CURRENT_CONGRESSIONAL_DISTRICT_VINTAGE, ) from microcosm.build.us_runtime.h5_io import ( + LEGACY_NULLABLE_STAGING_ARTIFACT_KIND, US_MULTISPINE_AGREEMENT_DIAGNOSTICS_ARTIFACT_KIND, US_MULTISPINE_POOL_H5_ARTIFACT_KIND, US_MULTISPINE_POOL_H5_MATERIALIZER_VERSION, US_MULTISPINE_POOL_MANIFEST_ARTIFACT_KIND, US_MULTISPINE_POOL_MANIFEST_SCHEMA_VERSION, US_STACKED_POOL_OPERATOR_ORDER, + AuthenticatedPoolH5, AuthenticatedPoolH5MismatchError, + identify_us_multispine_pool_manifest, + load_authenticated_us_multispine_pool_for_release, load_authenticated_us_multispine_pool_for_scoring, load_simulation_ready_us_multispine_pool, + require_authenticated_us_multispine_pool_h5, + us_multispine_pool_release_receipt, write_nullable_us_h5, ) from microcosm.build.us_runtime.support_provenance import ( @@ -1479,16 +1485,164 @@ def test_scoring_pool_loader_authenticates_failed_stacked_terminal_receipt( with pytest.raises(ValueError, match="not simulation-ready"): load_simulation_ready_us_multispine_pool(manifest_path) + with pytest.raises(ValueError, match="not simulation-ready"): + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=False, + ) frame, loaded_manifest, authenticated_h5 = ( load_authenticated_us_multispine_pool_for_scoring(manifest_path) ) + release_frame, release_manifest, release_h5 = ( + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=True, + ) + ) assert frame.n("household") == 3 assert loaded_manifest["status"] == "gate_failed" assert loaded_manifest["simulation_ready"] is False assert loaded_manifest["terminal_gates"]["passed"] is False assert authenticated_h5.sha256 == loaded_manifest["pool_h5"]["sha256"] + assert release_frame.n("household") == frame.n("household") + assert release_manifest == loaded_manifest + assert release_h5 == authenticated_h5 + + receipt = us_multispine_pool_release_receipt( + loaded_manifest, + authenticated_h5, + allow_gate_failed_base_pool=True, + ) + assert receipt["status"] == "gate_failed" + assert receipt["simulation_ready"] is False + assert receipt["allow_gate_failed_base_pool"] is True + assert receipt["agreement_gate_reference"] == { + "battery_status": "red", + "passed": False, + "gates_json_sha256": loaded_manifest["agreement_diagnostics"]["sha256"], + "failure_count": 1, + "failures": [ + { + "gate": "us_spine_agreement", + "message": "fixture terminal failure", + } + ], + "verdict": failed_gate, + } + + +def test_gate_failed_release_opt_in_is_rejected_for_a_ready_pool( + tmp_path: Path, +) -> None: + pytest.importorskip("tables") + manifest_path = _write_ready_pool(tmp_path, stacked=True) + _frame, manifest, authenticated_h5 = ( + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=False, + ) + ) + + with pytest.raises(ValueError, match="override is valid only"): + us_multispine_pool_release_receipt( + manifest, + authenticated_h5, + allow_gate_failed_base_pool=True, + ) + + +@pytest.mark.parametrize( + ("nested_passed", "failures"), + ((False, []), (True, ["contradictory failure"])), +) +def test_release_receipt_rejects_incoherent_nested_gate_verdict( + tmp_path: Path, + nested_passed: bool, + failures: list[str], +) -> None: + pytest.importorskip("tables") + manifest_path = _write_ready_pool(tmp_path, stacked=True) + _frame, manifest, authenticated_h5 = ( + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=False, + ) + ) + first_gate = next(iter(manifest["agreement_gate"]["gates"].values())) + first_gate["passed"] = nested_passed + first_gate["failures"] = failures + + with pytest.raises(ValueError, match="incoherent passed verdict"): + us_multispine_pool_release_receipt( + manifest, + authenticated_h5, + allow_gate_failed_base_pool=False, + ) + + +def test_pool_h5_identity_requires_its_missing_manifest_sidecar( + tmp_path: Path, +) -> None: + pytest.importorskip("tables") + manifest_path = _write_ready_pool(tmp_path, stacked=True) + pool_path = tmp_path / "pool.h5" + manifest_path.unlink() + + assert identify_us_multispine_pool_manifest(pool_path) == manifest_path + + +def test_pool_sidecar_identity_requires_authentication_even_without_h5_stamp( + tmp_path: Path, +) -> None: + pytest.importorskip("tables") + pool_path = tmp_path / "pool.h5" + write_nullable_us_h5( + _pool_frame(), + pool_path, + period=2024, + artifact_kind=LEGACY_NULLABLE_STAGING_ARTIFACT_KIND, + ) + manifest_path = pool_path.with_suffix(".manifest.json") + manifest_path.write_text( + json.dumps({"artifact_kind": US_MULTISPINE_POOL_MANIFEST_ARTIFACT_KIND}), + encoding="utf-8", + ) + + assert identify_us_multispine_pool_manifest(pool_path) == manifest_path + + +def test_non_pool_h5_has_no_required_pool_manifest(tmp_path: Path) -> None: + pytest.importorskip("tables") + h5_path = tmp_path / "staging.h5" + write_nullable_us_h5( + _pool_frame(), + h5_path, + period=2024, + artifact_kind=LEGACY_NULLABLE_STAGING_ARTIFACT_KIND, + ) + + assert identify_us_multispine_pool_manifest(h5_path) is None + + +def test_pool_sidecar_must_authenticate_the_requested_h5(tmp_path: Path) -> None: + requested = tmp_path / "requested.h5" + authenticated = tmp_path / "authenticated.h5" + identity = AuthenticatedPoolH5( + path=authenticated, + sha256="a" * 64, + size_bytes=1, + publication_run_id="fixture-publication", + manifest_sha256="b" * 64, + ) + + with pytest.raises(ValueError, match="authenticates a different H5"): + require_authenticated_us_multispine_pool_h5( + requested, + identity, + consumer="fixture consumer", + ) @pytest.mark.parametrize( diff --git a/packages/microcosm-build/tests/test_us_release_gate_preflight.py b/packages/microcosm-build/tests/test_us_release_gate_preflight.py index c9647f73..269cb615 100644 --- a/packages/microcosm-build/tests/test_us_release_gate_preflight.py +++ b/packages/microcosm-build/tests/test_us_release_gate_preflight.py @@ -16,9 +16,16 @@ from __future__ import annotations +import importlib.util +import json +from pathlib import Path + import numpy as np import pandas as pd +import pytest +import microcosm.build.us_runtime.release_gate_preflight as preflight_module +from microcosm.build.us_runtime.h5_io import AuthenticatedPoolH5 from microcosm.build.us_runtime.release_gate_preflight import ( PreflightReport, check_export_mass_parity_risk, @@ -36,6 +43,68 @@ from microcosm.frame import US_SCHEMA, Frame, WeightKind, Weights +def _load_preflight_cli(): + root = Path(__file__).resolve().parents[3] + path = root / "tools" / "preflight_us_release_gates.py" + spec = importlib.util.spec_from_file_location("preflight_us_release_gates", path) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(module) + return module + + +def _write_gate_failed_release_manifest( + path: Path, +) -> tuple[list[dict[str, str]], str]: + failure_messages = [ + "asec incidence differs", + "puf_tax_detail incidence differs", + ] + failures = [ + {"gate": "us_by_origin_battery", "message": message} + for message in failure_messages + ] + gates_sha256 = "2" * 64 + verdict = { + "passed": False, + "gates": { + "us_by_origin_battery": { + "passed": False, + "failures": failure_messages, + "details": {"fixture": True}, + } + }, + } + path.write_text( + json.dumps( + { + "schema_version": 1, + "build": { + "base_pool": { + "artifact_kind": "populace_us_multispine_input_pool", + "status": "gate_failed", + "simulation_ready": False, + "manifest_sha256": "a" * 64, + "publication_run_id": "fixture-publication", + "pool_h5_sha256": "1" * 64, + "pool_h5_size_bytes": 123, + "allow_gate_failed_base_pool": True, + "agreement_gate_reference": { + "passed": False, + "battery_status": "red", + "gates_json_sha256": gates_sha256, + "failure_count": len(failures), + "failures": failures, + "verdict": verdict, + }, + } + }, + } + ) + ) + return failures, gates_sha256 + + def _frame(households: list[dict[str, object]]) -> Frame: """A minimal US frame from household specs (one person per household).""" prows, hrows, spm_rows, mu_rows = [], [], [], [] @@ -639,8 +708,6 @@ def test__report__exit_code_and_status_precedence() -> None: def test__report__to_dict_is_json_ready() -> None: - import json - from microcosm.build.us_runtime.release_gate_preflight import CheckResult report = PreflightReport( @@ -660,6 +727,329 @@ def test__report__to_dict_is_json_ready() -> None: assert payload["checks"][0]["rows"] == [{"k": 1}] +def test__report__carried_red_pool_is_prominent_but_non_blocking(tmp_path) -> None: + release_manifest = tmp_path / "release_manifest.json" + failures, gates_sha256 = _write_gate_failed_release_manifest(release_manifest) + base_pool = json.loads(release_manifest.read_text())["build"]["base_pool"] + report = PreflightReport(checks=(), base_pool=base_pool) + + rendered = report.human_table() + assert report.status == "PASS" + assert report.exit_code == 0 + assert "BASE POOL BATTERY: RED — 2 FAILURES" in rendered + assert "Human publication decision required" in rendered + assert f"Gates JSON SHA-256: {gates_sha256}" in rendered + assert all( + f"[{failure['gate']}] {failure['message']}" in rendered + for failure in failures + ) + assert report.to_dict()["base_pool"] == base_pool + + +@pytest.mark.parametrize( + ( + "allow_gate_failed", + "status", + "simulation_ready", + "passed", + "rejected", + ), + ( + (False, "simulation_ready", True, True, False), + (True, "gate_failed", False, False, False), + (True, "simulation_ready", True, True, True), + ), +) +def test__preflight_base__passes_explicit_terminal_gate_policy_to_release_loader( + monkeypatch, + tmp_path, + allow_gate_failed, + status, + simulation_ready, + passed, + rejected, +) -> None: + base_h5 = tmp_path / "pool.h5" + base_h5.write_bytes(b"authenticated pool") + manifest_path = base_h5.with_suffix(".manifest.json") + failures = [] if passed else ["fixture battery failure"] + manifest = { + "status": status, + "simulation_ready": simulation_ready, + "agreement_gate": { + "passed": passed, + "gates": { + "us_by_origin_battery": { + "passed": passed, + "failures": failures, + } + }, + }, + "agreement_diagnostics": {"sha256": "2" * 64}, + } + identity = AuthenticatedPoolH5( + path=base_h5.resolve(), + sha256="1" * 64, + size_bytes=base_h5.stat().st_size, + publication_run_id="fixture-publication", + manifest_sha256="a" * 64, + ) + frame = _frame(_POOL) + monkeypatch.setattr( + preflight_module, + "identify_us_multispine_pool_manifest", + lambda path: manifest_path, + ) + + def selected(path, *, allow_terminal_gate_failure): + assert path == manifest_path + assert allow_terminal_gate_failure is allow_gate_failed + return frame, manifest, identity + + monkeypatch.setattr( + preflight_module, + "load_authenticated_us_multispine_pool_for_release", + selected, + ) + + if rejected: + with pytest.raises(ValueError, match="override is valid only"): + preflight_module._load_preflight_base( + base_h5, + allow_gate_failed_base_pool=allow_gate_failed, + ) + return + + loaded, receipt, loaded_manifest_path = preflight_module._load_preflight_base( + base_h5, + allow_gate_failed_base_pool=allow_gate_failed, + ) + + assert loaded is frame + assert loaded_manifest_path == manifest_path + assert receipt["status"] == status + assert receipt["agreement_gate_reference"]["failure_count"] == len(failures) + + +def test__preflight_base__refuses_actual_red_sidecar_without_opt_in( + monkeypatch, tmp_path +) -> None: + import microcosm.build.us_runtime.l0_refit_export as l0_refit_export + + base_h5 = tmp_path / "pool.h5" + base_h5.write_bytes(b"pool identity comes from its sibling manifest") + manifest_path = base_h5.with_suffix(".manifest.json") + manifest_path.write_text( + json.dumps( + { + "artifact_kind": "populace_us_multispine_pool_manifest", + "status": "gate_failed", + "simulation_ready": False, + } + ) + ) + monkeypatch.setattr( + l0_refit_export, + "load_us_frame", + lambda path: pytest.fail("red pool reached the generic H5 loader"), + ) + + with pytest.raises(ValueError, match="not simulation-ready"): + preflight_module._load_preflight_base( + base_h5, + allow_gate_failed_base_pool=False, + ) + + +def test__preflight_base__refuses_bare_stamped_pool_before_generic_load( + monkeypatch, tmp_path +) -> None: + pytest.importorskip("tables", exc_type=ModuleNotFoundError) + import microcosm.build.us_runtime.l0_refit_export as l0_refit_export + + base_h5 = tmp_path / "pool.h5" + with pd.HDFStore(base_h5, mode="w") as store: + store.put( + "_populace_staging_metadata", + pd.Series( + [ + json.dumps( + { + "artifact_kind": "populace_us_multispine_input_pool", + "household_weight_kind": "importance", + "publication_run_id": "fixture-publication", + } + ) + ] + ), + format="table", + ) + monkeypatch.setattr( + l0_refit_export, + "load_us_frame", + lambda path: pytest.fail("bare pool reached the generic H5 loader"), + ) + + with pytest.raises(ValueError, match="pool.manifest.json is not readable"): + preflight_module._load_preflight_base( + base_h5, + allow_gate_failed_base_pool=False, + ) + + +def test__cli__release_manifest_does_not_replace_required_static_inputs( + monkeypatch, tmp_path, capsys +) -> None: + cli = _load_preflight_cli() + release_manifest = tmp_path / "release_manifest.json" + _write_gate_failed_release_manifest(release_manifest) + monkeypatch.setattr( + cli, + "run_preflight", + lambda **kwargs: pytest.fail("argument validation reached static checks"), + ) + + with pytest.raises(SystemExit) as error: + cli.main(["--release-manifest", str(release_manifest)]) + + assert error.value.code == 2 + stderr = capsys.readouterr().err + assert "--base-h5" in stderr + assert "--selection-source-manifest" in stderr + + +@pytest.mark.parametrize( + ("static_status", "expected_exit"), + (("PASS", 0), ("AT_RISK", 2), ("FAIL", 1)), +) +def test__cli__carried_red_battery_does_not_change_existing_preflight_exit( + monkeypatch, + tmp_path, + capsys, + static_status, + expected_exit, +) -> None: + from microcosm.build.us_runtime.release_gate_preflight import CheckResult + + cli = _load_preflight_cli() + release_manifest = tmp_path / "release_manifest.json" + failures, gates_sha256 = _write_gate_failed_release_manifest(release_manifest) + base_pool = json.loads(release_manifest.read_text())["build"]["base_pool"] + json_out = tmp_path / "preflight.json" + report = PreflightReport( + checks=( + CheckResult( + name="existing_check", + status=static_status, + summary="existing check controls the exit", + failures=("fixture failure",) if static_status == "FAIL" else (), + at_risks=("fixture risk",) if static_status == "AT_RISK" else (), + ), + ), + base_pool=base_pool, + ) + captured: dict[str, object] = {} + + def fake_run_preflight(**kwargs): + captured.update(kwargs) + return report + + monkeypatch.setattr(cli, "run_preflight", fake_run_preflight) + + exit_code = cli.main( + [ + "--base-h5", + str(tmp_path / "base.h5"), + "--selection-source-manifest", + str(tmp_path / "selection.json"), + "--allow-gate-failed-base-pool", + "--release-manifest", + str(release_manifest), + "--json-out", + str(json_out), + ] + ) + + output = capsys.readouterr().out + payload = json.loads(json_out.read_text()) + assert exit_code == report.exit_code == expected_exit + assert output.startswith("=" * 72) + assert "CARRIED BASE-POOL AGREEMENT BATTERY: RED — 2 FAILURES" in output + assert "Build opt-in used: --allow-gate-failed-base-pool" in output + assert f"Pool gates JSON SHA-256: {gates_sha256}" in output + assert "Publication decision: HUMAN REVIEW REQUIRED" in output + assert "does not alter the preflight exit code" in output + assert all( + f"FAILURE [{failure['gate']}]: {failure['message']}" in output + for failure in failures + ) + assert output.rstrip().endswith( + "CARRIED RED BATTERY: HUMAN REVIEW REQUIRED; automated preflight exit " + f"remains {expected_exit}." + ) + assert payload["exit_code"] == expected_exit + assert payload["carried_base_pool_agreement_battery"][ + "affects_exit_code" + ] is False + assert payload["carried_base_pool_agreement_battery"]["failures"] == failures + assert payload["carried_base_pool_agreement_battery"][ + "agreement_gate_reference" + ]["verdict"]["passed"] is False + assert captured["allow_gate_failed_base_pool"] is True + + +@pytest.mark.parametrize( + "mismatch", + ( + "generic_base", + "manifest_sha256", + "pool_h5_sha256", + "pool_h5_size_bytes", + "publication_run_id", + "gates_json_sha256", + "verdict", + ), +) +def test__cli__release_manifest_must_match_authenticated_base_pool( + monkeypatch, + tmp_path, + mismatch, +) -> None: + cli = _load_preflight_cli() + release_manifest = tmp_path / "release_manifest.json" + _write_gate_failed_release_manifest(release_manifest) + authenticated = json.loads( + json.dumps(json.loads(release_manifest.read_text())["build"]["base_pool"]) + ) + if mismatch == "generic_base": + authenticated = None + elif mismatch == "pool_h5_size_bytes": + authenticated[mismatch] += 1 + elif mismatch == "gates_json_sha256": + authenticated["agreement_gate_reference"][mismatch] = "3" * 64 + elif mismatch == "verdict": + authenticated["agreement_gate_reference"]["verdict"]["gates"][ + "us_by_origin_battery" + ]["details"] = {"fixture": "different"} + else: + authenticated[mismatch] = "different-authenticated-value" + report = PreflightReport(checks=(), base_pool=authenticated) + monkeypatch.setattr(cli, "run_preflight", lambda **kwargs: report) + + with pytest.raises(ValueError, match="does not exactly match"): + cli.main( + [ + "--base-h5", + str(tmp_path / "base.h5"), + "--selection-source-manifest", + str(tmp_path / "selection.json"), + "--allow-gate-failed-base-pool", + "--release-manifest", + str(release_manifest), + ] + ) + + def test__load_ledger_target_specs__hands_fact_rows_to_the_compiler( monkeypatch, tmp_path ) -> None: diff --git a/tools/build_us_fiscal_refresh_release.py b/tools/build_us_fiscal_refresh_release.py index b00c5189..df0634b2 100644 --- a/tools/build_us_fiscal_refresh_release.py +++ b/tools/build_us_fiscal_refresh_release.py @@ -228,7 +228,11 @@ ) from microcosm.build.us_runtime.h5_io import ( AuthenticatedPoolH5, + identify_us_multispine_pool_manifest, + load_authenticated_us_multispine_pool_for_release, load_simulation_ready_us_multispine_pool, + require_authenticated_us_multispine_pool_h5, + us_multispine_pool_release_receipt, ) from microcosm.build.us_runtime.input_mass import us_input_mass_totals from microcosm.build.us_runtime.l0_refit_export import ( @@ -848,6 +852,18 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: "Mutually exclusive with --base-h5." ), ) + parser.add_argument( + "--allow-gate-failed-base-pool", + action="store_true", + help=( + "Explicitly allow --base-h5 to consume an authenticated current " + "stacked pool with status=gate_failed and simulation_ready=false. " + "The red agreement-gate verdict is carried into the release " + "manifest for a separate human publication decision. Without " + "this flag an identified pool H5 remains fail-closed. The exact-k " + "--pool-manifest arm is always simulation-ready-only." + ), + ) parser.add_argument( "--pool-manifest-sha256", help=( @@ -1537,6 +1553,8 @@ def _parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: ) if args.evidence_failure_owners is not None and not args.evidence_release: parser.error("--evidence-failure-owners requires --evidence-release.") + if args.allow_gate_failed_base_pool and args.base_h5 is None: + parser.error("--allow-gate-failed-base-pool requires --base-h5.") if args.evidence_release and args.exact_k is not None: parser.error( "--evidence-release is incompatible with --exact-k: ladder " @@ -1675,6 +1693,41 @@ def _legacy_base_h5_sha256(path: Path) -> str: return _sha256(path) +def _load_base_pool_if_identified( + path: Path, + *, + allow_gate_failed_base_pool: bool, +) -> tuple[Frame | None, dict[str, object] | None, AuthenticatedPoolH5 | None]: + """Authenticate a pool supplied through legacy ``--base-h5`` if present.""" + + manifest_path = identify_us_multispine_pool_manifest(path) + if manifest_path is None: + if allow_gate_failed_base_pool: + raise ValueError( + "--allow-gate-failed-base-pool was set, but --base-h5 does not " + "identify as a US multispine pool." + ) + return None, None, None + + frame, manifest, authenticated_pool_h5 = ( + load_authenticated_us_multispine_pool_for_release( + manifest_path, + allow_terminal_gate_failure=allow_gate_failed_base_pool, + ) + ) + require_authenticated_us_multispine_pool_h5( + path, + authenticated_pool_h5, + consumer="US fiscal refresh release builder --base-h5", + ) + receipt = us_multispine_pool_release_receipt( + manifest, + authenticated_pool_h5, + allow_gate_failed_base_pool=allow_gate_failed_base_pool, + ) + return frame, receipt, authenticated_pool_h5 + + def _copy_base_h5_for_local_audit( source: Path, destination: Path, @@ -7460,6 +7513,7 @@ def _build_manifests( calibration_key: str = "populace_us_2024_calibration", calibration_filename: str = CALIBRATION_FILENAME, exact_k_ladder: Mapping[str, object] | None = None, + base_pool: Mapping[str, object] | None = None, evidence_known_failures: Sequence[Mapping[str, str]] | None = None, ) -> None: dataset_path = artifact_root / dataset_filename @@ -7526,6 +7580,7 @@ def _build_manifests( if exact_k_ladder is not None else {} ), + **({"base_pool": dict(base_pool)} if base_pool is not None else {}), "dataset": { "filename": dataset_filename, "sha256": dataset_sha, @@ -7749,6 +7804,7 @@ def _build_manifests( if exact_k_ladder is not None else {} ), + **({"base_pool": dict(base_pool)} if base_pool is not None else {}), "warm_start_calibration": warm_start_payload, "selection_source": selection_source_payload, "default_dataset": default_dataset_payload, @@ -8532,6 +8588,7 @@ def _main(argv: Sequence[str] | None = None) -> None: pool_original_household_ids: np.ndarray | None = None pool_original_household_weights: np.ndarray | None = None pool_manifest_payload: dict[str, object] | None = None + base_pool_receipt: dict[str, object] | None = None authenticated_pool_h5: AuthenticatedPoolH5 | None = None if args.pool_manifest is not None: pool_frame, pool_manifest_payload, authenticated_pool_h5 = ( @@ -8564,7 +8621,19 @@ def _main(argv: Sequence[str] | None = None) -> None: base_h5 = authenticated_pool_h5.path else: base_h5 = args.base_h5 or _download_base_h5() - base_dataset_sha256 = _legacy_base_h5_sha256(base_h5) + pool_frame, base_pool_receipt, authenticated_pool_h5 = ( + _load_base_pool_if_identified( + base_h5, + allow_gate_failed_base_pool=args.allow_gate_failed_base_pool, + ) + ) + if authenticated_pool_h5 is None: + base_dataset_sha256 = _legacy_base_h5_sha256(base_h5) + else: + base_dataset_sha256 = authenticated_pool_h5.verified_digest( + consumer="builder base dataset identity" + ) + base_h5 = authenticated_pool_h5.path digest = base_dataset_sha256[:7] build_timestamp = datetime.now(UTC) full_commit = _git_output("rev-parse", "HEAD") @@ -8912,8 +8981,9 @@ def _main(argv: Sequence[str] | None = None) -> None: "method": "preserve_validated_multispine_pool_weights", "applied": False, "reason": ( - "Exact-k selection and HT-with-q refit retain the validated " - "pool artifact's original importance-weight baseline." + "The authenticated multispine input retains the published " + "pool artifact's original importance-weight baseline; " + "pool-owned preparation stages are not replayed." ), "initial_population": pool_population, "benchmark": US_BASE_PERSON_POPULATION_BENCHMARK, @@ -11721,6 +11791,7 @@ def _main(argv: Sequence[str] | None = None) -> None: calibration_key=calibration_key, calibration_filename=calibration_filename, exact_k_ladder=exact_k_ladder_provenance, + base_pool=base_pool_receipt, evidence_known_failures=evidence_known_failures, ) if telemetry is not None: diff --git a/tools/preflight_us_release_gates.py b/tools/preflight_us_release_gates.py index 0ca5ec8c..1cb2b2ce 100644 --- a/tools/preflight_us_release_gates.py +++ b/tools/preflight_us_release_gates.py @@ -17,7 +17,10 @@ --selection-source-manifest inputs/buildm_keogh_swap_selection_source.json \ --export-input-mass-reference-h5 forensics/populace_us_2024.h5 -Exit code: 1 on any FAIL, 2 on AT-RISK only, 0 clean. +Exit code: 1 on any static-check FAIL, 2 on static AT-RISK only, 0 clean. A +carried red base-pool battery is human-review evidence and does not by itself +change that exit code. When ``--release-manifest`` is supplied, its base-pool +receipt must exactly match the pool authenticated by this preflight. """ from __future__ import annotations @@ -34,10 +37,191 @@ 0, str(Path(__file__).resolve().parents[1] / "packages" / "microcosm-build" / "src") ) +from microcosm.build.us_runtime.h5_io import ( # noqa: E402 + US_MULTISPINE_POOL_H5_ARTIFACT_KIND, +) from microcosm.build.us_runtime.release_gate_preflight import ( # noqa: E402 run_preflight, ) +_ALLOW_GATE_FAILED_BASE_POOL_FLAG = "--allow-gate-failed-base-pool" +_CARRIED_BATTERY_PAYLOAD_KEY = "carried_base_pool_agreement_battery" + + +def _json_object(value: object, *, label: str) -> dict[str, object]: + if not isinstance(value, dict): + raise ValueError(f"{label} must be a JSON object.") + return value + + +def _load_release_base_pool_receipt(path: Path) -> dict[str, object] | None: + """Read one optional base-pool receipt from a built release manifest.""" + + release_manifest = _json_object( + json.loads(path.read_text()), + label=f"release manifest {path}", + ) + build = release_manifest.get("build") + if not isinstance(build, dict): + raise ValueError(f"Release manifest {path} has no build object.") + if "base_pool" not in build: + return None + base_pool = build.get("base_pool") + if not isinstance(base_pool, dict): + raise ValueError( + f"Release manifest {path} build.base_pool must be a JSON object." + ) + return base_pool + + +def _carried_base_pool_battery( + base_pool: dict[str, object] | None, + *, + path: Path, +) -> dict[str, object] | None: + """Validate the non-blocking red verdict carried by one pool receipt.""" + + if base_pool is None: + return None + agreement_gate_reference = base_pool.get("agreement_gate_reference") + carries_gate_failed_override = ( + base_pool.get("allow_gate_failed_base_pool") is True + or base_pool.get("status") == "gate_failed" + or ( + isinstance(agreement_gate_reference, dict) + and agreement_gate_reference.get("battery_status") == "red" + ) + ) + if not carries_gate_failed_override: + return None + if ( + base_pool.get("artifact_kind") != US_MULTISPINE_POOL_H5_ARTIFACT_KIND + or base_pool.get("status") != "gate_failed" + or base_pool.get("simulation_ready") is not False + or base_pool.get("allow_gate_failed_base_pool") is not True + ): + raise ValueError( + f"Release manifest {path} has an incoherent gate-failed base-pool " + "carriage receipt." + ) + gate_reference = _json_object( + agreement_gate_reference, + label=( + f"release manifest {path} build.base_pool.agreement_gate_reference" + ), + ) + failures = gate_reference.get("failures") + failure_count = gate_reference.get("failure_count") + gates_json_sha256 = gate_reference.get("gates_json_sha256") + verdict = gate_reference.get("verdict") + if ( + gate_reference.get("passed") is not False + or gate_reference.get("battery_status") != "red" + or not isinstance(failures, list) + or not all( + isinstance(failure, dict) + and isinstance(failure.get("gate"), str) + and bool(failure.get("gate")) + and isinstance(failure.get("message"), str) + for failure in failures + ) + or type(failure_count) is not int + or failure_count != len(failures) + or not isinstance(gates_json_sha256, str) + or len(gates_json_sha256) != 64 + or any( + character not in "0123456789abcdef" + for character in gates_json_sha256 + ) + or not isinstance(verdict, dict) + or verdict.get("passed") is not False + ): + raise ValueError( + f"Release manifest {path} has an incomplete or inconsistent " + "carried red agreement-battery verdict." + ) + verdict_gates = verdict.get("gates") + if not isinstance(verdict_gates, dict): + raise ValueError( + f"Release manifest {path} has no full carried gate verdict." + ) + verdict_failures: list[dict[str, str]] = [] + for gate_name, gate_payload in verdict_gates.items(): + if not isinstance(gate_name, str) or not isinstance(gate_payload, dict): + raise ValueError( + f"Release manifest {path} has a malformed carried gate verdict." + ) + gate_failures = gate_payload.get("failures") + gate_passed = gate_payload.get("passed") + if type(gate_passed) is not bool or not isinstance( + gate_failures, list + ) or not all( + isinstance(failure, str) for failure in gate_failures + ): + raise ValueError( + f"Release manifest {path} has a malformed carried failure list." + ) + if gate_passed is bool(gate_failures): + raise ValueError( + f"Release manifest {path} has an incoherent nested gate verdict." + ) + verdict_failures.extend( + {"gate": gate_name, "message": failure} for failure in gate_failures + ) + if not verdict_failures or verdict_failures != failures: + raise ValueError( + f"Release manifest {path} failure summary does not match its full " + "carried gate verdict." + ) + return { + "battery_status": "red", + "pool_status": "gate_failed", + "simulation_ready": False, + "allow_gate_failed_base_pool": True, + "flag": _ALLOW_GATE_FAILED_BASE_POOL_FLAG, + "gates_json_sha256": gates_json_sha256, + "failure_count": failure_count, + "failures": [dict(failure) for failure in failures], + "agreement_gate_reference": dict(gate_reference), + "publication_decision": "human_review_required", + "affects_exit_code": False, + } + + +def _require_matching_release_base_pool( + authenticated: dict[str, object] | None, + carried: dict[str, object] | None, + *, + path: Path, +) -> None: + """Bind a release manifest to the exact pool preflight authenticated.""" + + if authenticated != carried: + raise ValueError( + f"Release manifest {path} build.base_pool does not exactly match " + "the base-pool receipt authenticated by this preflight." + ) + + +def _carried_battery_banner(carried: dict[str, object]) -> str: + count = int(carried["failure_count"]) + noun = "FAILURE" if count == 1 else "FAILURES" + lines = [ + "=" * 72, + f"CARRIED BASE-POOL AGREEMENT BATTERY: RED — {count} {noun}", + "Pool status: gate_failed; simulation_ready: false", + f"Build opt-in used: {carried['flag']}", + f"Pool gates JSON SHA-256: {carried['gates_json_sha256']}", + "Publication decision: HUMAN REVIEW REQUIRED", + "This carried verdict does not alter the preflight exit code.", + ] + lines.extend( + f" FAILURE [{failure['gate']}]: {failure['message']}" + for failure in carried["failures"] + ) + lines.append("=" * 72) + return "\n".join(lines) + def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( @@ -53,12 +237,31 @@ def _parser() -> argparse.ArgumentParser: type=Path, help="Base pool H5 (read-only).", ) + parser.add_argument( + "--allow-gate-failed-base-pool", + action="store_true", + help=( + "Allow an authenticated current stacked --base-h5 pool whose " + "terminal battery is red. The verdict is displayed prominently " + "but does not itself determine the preflight exit code." + ), + ) parser.add_argument( "--selection-source-manifest", required=True, type=Path, help="Frozen selection-source manifest JSON.", ) + parser.add_argument( + "--release-manifest", + type=Path, + help=( + "Optional built release_manifest.json. A carried gate-failed " + "base-pool battery is displayed as prominent, non-blocking " + "evidence for the human publication decision after its receipt " + "is matched to the authenticated --base-h5 pool." + ), + ) parser.add_argument( "--export-input-mass-reference-h5", type=Path, @@ -119,6 +322,20 @@ def _parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: args = _parser().parse_args(argv) + release_base_pool = ( + _load_release_base_pool_receipt(args.release_manifest) + if args.release_manifest is not None + else None + ) + carried = ( + _carried_base_pool_battery( + release_base_pool, + path=args.release_manifest, + ) + if args.release_manifest is not None + else None + ) + try: target_period: int | str = int(args.target_period) except (TypeError, ValueError): @@ -133,18 +350,34 @@ def main(argv: list[str] | None = None) -> int: target_period=target_period, relative_tolerance=args.relative_tolerance, minimum_reference_total=args.minimum_reference_total, + allow_gate_failed_base_pool=args.allow_gate_failed_base_pool, ) - + if args.release_manifest is not None: + _require_matching_release_base_pool( + report.base_pool, + release_base_pool, + path=args.release_manifest, + ) + if carried is not None: + print(_carried_battery_banner(carried)) print(report.human_table()) payload = report.to_dict() + exit_code = report.exit_code + if carried is not None: + payload[_CARRIED_BATTERY_PAYLOAD_KEY] = carried if args.json_out is not None: args.json_out.write_text(json.dumps(payload, indent=2, sort_keys=False)) print(f"\nWrote machine-readable report to {args.json_out}") else: print("\n--- machine-readable report ---") print(json.dumps(payload, indent=2, sort_keys=False)) + if carried is not None: + print( + "\nCARRIED RED BATTERY: HUMAN REVIEW REQUIRED; " + f"automated preflight exit remains {exit_code}." + ) - return report.exit_code + return exit_code if __name__ == "__main__":