diff --git a/local-candidates/flow-1.35.0-measured/.gitignore b/local-candidates/flow-1.35.0-measured/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/local-candidates/flow-1.35.0-measured/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/local-candidates/flow-1.35.0-measured/README.md b/local-candidates/flow-1.35.0-measured/README.md new file mode 100644 index 0000000..bcb4640 --- /dev/null +++ b/local-candidates/flow-1.35.0-measured/README.md @@ -0,0 +1,76 @@ +# Flow 1.35.0 measured admission candidate + +The retained 1.35.0 evidence refuses admission. The default attended continuation +omits its execution target, and the repair campaign cannot qualify the encrypted +anchor path. Each gap has three retained reproductions. The 33 base-condition +trials and three idempotency trials passed their expected outcomes. + +This directory prepares an unsigned release candidate for review. `prepare.py` +checks the published wheel and sdist, their GitHub asset digests, the annotated +tag, and the successful qualification and three-OS lifecycle runs for source +`6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c`. + +The measured evidence must come from the installed 1.35.0 wheel. The script +checks every retained file against its manifest, rejects repeated trial +identities and reused observations, and derives the campaign totals from the +individual trials. Missing classes, omitted counters, altered files, an +incorrect runtime, or a failed admission condition stop preparation. + +```bash +python local-candidates/flow-1.35.0-measured/prepare.py \ + --measured-manifest /absolute/path/to/measured-manifest.json \ + --out local-candidates/flow-1.35.0-measured/candidate.json +``` + +Without `--measured-manifest`, the output is an `evidence-incomplete` release +inventory. It cannot assert measured acceptance. The tests in +`tests/test_measured_flow_candidate.py` exercise the validator with explicit +test fixtures; they aren't qualification trials. + +The evidence scope is the local synthetic SQLite and pixel fixture. Synthetic +reviewer inputs exercise the real decision APIs. They do not establish customer +qualification, an actual human review, or approval to issue an admission. + +`prepare.py` never reads a signing key or changes a registry. After review, the +existing issuer must bind the exact retained manifest and contract digests, +preserve the previous admission in sequence, and issue the matching signed +objects. The live ledger and its projections require a separate reviewed +change. Flow 1.34.0 remains the published admission until that change succeeds. + +The corrected successor must use a new immutable version. To inspect an +unpublished 1.35.1 wheel and sdist with their measured evidence: + +```bash +python local-candidates/flow-1.35.0-measured/prepare.py \ + --unpublished-wheel /absolute/path/to/openadapt_flow-1.35.1-py3-none-any.whl \ + --unpublished-sdist /absolute/path/to/openadapt_flow-1.35.1.tar.gz \ + --source-commit EXACT_BUILD_COMMIT \ + --measured-manifest /absolute/path/to/1.35.1/evidence-manifest.json \ + --out local-candidates/flow-1.35.1-measured/candidate.json +``` + +This mode verifies the local archive metadata and hashes. The source commit +comes from the build record and requires review against the retained build +provenance. Even complete measured evidence produces only +`ready-for-release-review`. It records publication as false and retains the +required merge, protected-main qualification, three-OS lifecycle, immutable +publication, artifact parity, and admission steps. The 1.35.0 refusal stays +separate from the successor's evidence. + +For a complete candidate, every counted trial must retain a normalized +observation with the same identity and counters as its manifest entry. The +verifier reads the referenced native reports and checks their outcome, success +flag, and model-call counts. Each uncertain-delivery, attended, and repair +trial must satisfy its own event requirements. Events from one trial cannot +satisfy another trial's requirement. + +Attended acceptance requires a native `VERIFIED` effect report. A receipt that +only reports decision completion cannot replace that effect proof. The wheel +check compares every inventoried runtime member with the retained installation +proof and rejects omitted members or extra installed files. + +These checks establish consistency among the retained bytes. They don't prove +that an execution took place. Reviewers must inspect the retained measurement +code, installation provenance, and independent oracle references before they +accept the observation. The public verifier does not contain private oracle +recipes or grant authority to its test fixtures. diff --git a/local-candidates/flow-1.35.0-measured/candidate.json b/local-candidates/flow-1.35.0-measured/candidate.json new file mode 100644 index 0000000..8210476 --- /dev/null +++ b/local-candidates/flow-1.35.0-measured/candidate.json @@ -0,0 +1,198 @@ +{ + "admission_issued": false, + "canonical_policy_commit": "9b61391713203998e584f4834e4812bb41b40f6e", + "evidence_class": "remote-safe-synthetic", + "limitations": [ + { + "detail": "BoundAttendedExecutor continuation omits execution_target_kind. The qualified default continuation cannot observe the environment and refuses after successful live revalidation.", + "id": "attended-resume-target-kind", + "status": "observed integration failure" + }, + { + "detail": "The repair campaign reads plaintext template/context inputs. It fails campaign proof for the actual encrypted, template-armed OCR repair candidate and refuses approval.", + "id": "encrypted-repair-anchor", + "status": "observed integration failure" + } + ], + "measured_manifest_sha256": "sha256:6b7ee4704e42776f707f4b176996b08ffa6bfda1501bc68b65faf7b3dbb59cff", + "measured_scope": { + "application": "Pillow synthetic clinic fixture", + "operator_principal": "Explicit synthetic local protocol inputs; no real human/authentication provider proof", + "required_path": "Default real attended and encrypted repair paths; added deployment adapters/plaintext diagnostics do not substitute for those paths", + "runtime": "Exact installed unmodified Flow 1.35.0 wheel", + "system_of_record": "Local SQLite" + }, + "proposed_release_identity": { + "channel": "production", + "previous_admission_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", + "schema_version": "openadapt.monotonic-production-release/v1", + "sequence": 2 + }, + "release_observation": { + "artifacts": [ + { + "github_url": "https://github.com/OpenAdaptAI/openadapt-flow/releases/download/v1.35.0/openadapt_flow-1.35.0-py3-none-any.whl", + "kind": "python-wheel", + "name": "openadapt_flow-1.35.0-py3-none-any.whl", + "pypi_url": "https://files.pythonhosted.org/packages/09/89/6c00cae30e9a75f5c2473178550012a8afeeab83c9f465e3039b051a2374/openadapt_flow-1.35.0-py3-none-any.whl", + "sha256": "sha256:2bd0122d506aade41a018cdac1f06a139adef7e85d4ca0b186bb56ee94dc3297", + "size_bytes": 2121501 + }, + { + "github_url": "https://github.com/OpenAdaptAI/openadapt-flow/releases/download/v1.35.0/openadapt_flow-1.35.0.tar.gz", + "kind": "python-sdist", + "name": "openadapt_flow-1.35.0.tar.gz", + "pypi_url": "https://files.pythonhosted.org/packages/d8/aa/8086768b5a0df9f901a972d4e2bce829a18a24429d2969bdcda1f0e48b5d/openadapt_flow-1.35.0.tar.gz", + "sha256": "sha256:45668042b346fd40ec881f8d43ed8b395c86aa075729168d4787877e03ee6ef3", + "size_bytes": 21116872 + } + ], + "observed_at": "2026-09-08T21:46:10.121623+00:00", + "release_url": "https://github.com/OpenAdaptAI/openadapt-flow/releases/tag/v1.35.0", + "runs": { + "qualification": { + "conclusion": "success", + "id": 33822314626, + "jobs": [ + { + "conclusion": "success", + "name": "docs-consistency" + }, + { + "conclusion": "success", + "name": "scope" + }, + { + "conclusion": "success", + "name": "windows-mock" + }, + { + "conclusion": "success", + "name": "effectbench-standalone" + }, + { + "conclusion": "success", + "name": "interop-types" + }, + { + "conclusion": "success", + "name": "python-compatibility" + }, + { + "conclusion": "success", + "name": "phi-guard" + }, + { + "conclusion": "success", + "name": "mypy-strict-safety" + }, + { + "conclusion": "success", + "name": "lint" + }, + { + "conclusion": "success", + "name": "linux-atspi-x11" + }, + { + "conclusion": "success", + "name": "test" + }, + { + "conclusion": "success", + "name": "wheel" + }, + { + "conclusion": "success", + "name": "e2e-browser" + }, + { + "conclusion": "success", + "name": "test-matrix (ubuntu-latest, 3.12)" + }, + { + "conclusion": "success", + "name": "test-matrix (ubuntu-latest, 3.11)" + }, + { + "conclusion": "success", + "name": "test-matrix (macos-latest, 3.12)" + }, + { + "conclusion": "success", + "name": "test-matrix (ubuntu-latest, 3.10)" + } + ], + "source_commit": "6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c", + "url": "https://github.com/OpenAdaptAI/openadapt-flow/actions/runs/33822314626" + }, + "release": { + "conclusion": "success", + "id": 33826859321, + "jobs": [ + { + "conclusion": "success", + "name": "validate-tag" + }, + { + "conclusion": "skipped", + "name": "backfill-github-release" + }, + { + "conclusion": "skipped", + "name": "validate-dispatch" + }, + { + "conclusion": "skipped", + "name": "create-release-tag" + }, + { + "conclusion": "success", + "name": "publish-tag" + } + ], + "source_commit": "6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c", + "url": "https://github.com/OpenAdaptAI/openadapt-flow/actions/runs/33826859321" + }, + "three_os_lifecycle": { + "conclusion": "success", + "id": 33822325382, + "jobs": [ + { + "conclusion": "success", + "name": "lifecycle (macos-latest)" + }, + { + "conclusion": "success", + "name": "lifecycle (ubuntu-latest)" + }, + { + "conclusion": "success", + "name": "lifecycle (windows-latest)" + } + ], + "source_commit": "6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c", + "url": "https://github.com/OpenAdaptAI/openadapt-flow/actions/runs/33822325382" + } + }, + "source_commit": "6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c", + "tag": "v1.35.0", + "tag_object": "4e851c65e8fbfd676c2851360f567e2bb7af4945", + "version": "1.35.0" + }, + "required_campaign_classes": [ + "healthy", + "safe_halt", + "idempotency_replay", + "uncertain_delivery", + "declared_attended", + "governed_repair" + ], + "schema_version": "openadapt.unsigned-measured-release-candidate/v1", + "scope": "Executed local synthetic SQLite and pixel fixture; synthetic reviewer inputs; no customer qualification or actual human approval.", + "state": "evidence-incomplete", + "target": "flow", + "validation_errors": [ + "measured trials are missing for declared_attended, governed_repair" + ] +} diff --git a/local-candidates/flow-1.35.0-measured/prepare.py b/local-candidates/flow-1.35.0-measured/prepare.py new file mode 100644 index 0000000..f9efd4b --- /dev/null +++ b/local-candidates/flow-1.35.0-measured/prepare.py @@ -0,0 +1,525 @@ +#!/usr/bin/env python3 +"""Prepare an unsigned Flow release candidate from measured trials. + +No key access, signatures, registry writes, or publication occur here. The +operator must review the exact candidate before the existing issuer uses it. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import subprocess +import tarfile +import zipfile +from email.parser import BytesParser +import sys +from collections import Counter, defaultdict +from datetime import datetime, timezone +from pathlib import Path +from urllib.request import urlopen + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "scripts")) +import production_trust as trust # noqa: E402 + +VERSION = "1.35.0" +SOURCE = "6d1f80aa4775e6aa398c3251c8d53e7ec61d0c0c" +REPOSITORY = "OpenAdaptAI/openadapt-flow" +RUNS = { + "release": 33826859321, + "qualification": 33822314626, + "three_os_lifecycle": 33822325382, +} +DERIVED_FIELDS = { + "task_condition_cell_count", + "minimum_trials_per_cell", + "observed_trial_count", +} +COUNTERS = trust.CAMPAIGN_COUNT_FIELDS - DERIVED_FIELDS + + +def sha(raw: bytes) -> str: + return "sha256:" + hashlib.sha256(raw).hexdigest() + + +def read_json(path: Path): + return json.loads(path.read_bytes()) + + +def gh(path: str): + return json.loads(subprocess.check_output(["gh", "api", path], text=True)) + + +def observe_release() -> dict: + with urlopen( + f"https://pypi.org/pypi/openadapt-flow/{VERSION}/json", timeout=30 + ) as response: + pypi = json.load(response) + files = pypi["urls"] + if len(files) != 2 or {f["packagetype"] for f in files} != {"bdist_wheel", "sdist"}: + raise ValueError("the release must contain exactly one wheel and one sdist") + release = gh(f"repos/{REPOSITORY}/releases/tags/v{VERSION}") + if release["draft"] or release["prerelease"]: + raise ValueError("the GitHub release is not a stable public release") + ref = gh(f"repos/{REPOSITORY}/git/ref/tags/v{VERSION}")["object"] + if ref["type"] != "tag": + raise ValueError("expected the annotated release tag") + tag = gh(f"repos/{REPOSITORY}/git/tags/{ref['sha']}") + if tag["object"] != { + "sha": SOURCE, + "type": "commit", + "url": f"https://api.github.com/repos/{REPOSITORY}/git/commits/{SOURCE}", + }: + raise ValueError("the release tag source changed") + artifacts = [] + for item in sorted(files, key=lambda f: f["filename"]): + if item["yanked"]: + raise ValueError("a release artifact is yanked") + asset = [a for a in release["assets"] if a["name"] == item["filename"]] + expected_digest = "sha256:" + item["digests"]["sha256"] + if ( + len(asset) != 1 + or asset[0]["state"] != "uploaded" + or asset[0]["size"] != item["size"] + or asset[0].get("digest") != expected_digest + ): + raise ValueError("GitHub and PyPI artifact inventory differs") + artifacts.append( + { + "name": item["filename"], + "sha256": expected_digest, + "size_bytes": item["size"], + "pypi_url": item["url"], + "github_url": asset[0]["browser_download_url"], + "kind": "python-wheel" + if item["packagetype"] == "bdist_wheel" + else "python-sdist", + } + ) + runs = {} + for name, run_id in RUNS.items(): + run = gh(f"repos/{REPOSITORY}/actions/runs/{run_id}") + if ( + run["head_sha"] != SOURCE + or run["status"] != "completed" + or run["conclusion"] != "success" + ): + raise ValueError(f"the exact-source {name} run did not pass") + jobs = gh(f"repos/{REPOSITORY}/actions/runs/{run_id}/jobs?per_page=100") + if jobs["total_count"] != len(jobs["jobs"]) or any( + j["conclusion"] not in {"success", "skipped"} for j in jobs["jobs"] + ): + raise ValueError(f"the {name} job inventory is incomplete or failed") + if name != "release" and any( + j["conclusion"] != "success" for j in jobs["jobs"] + ): + raise ValueError( + "every exact-release qualification job must execute successfully" + ) + runs[name] = { + "id": run_id, + "url": run["html_url"], + "source_commit": run["head_sha"], + "conclusion": run["conclusion"], + "jobs": [ + {"name": j["name"], "conclusion": j["conclusion"]} for j in jobs["jobs"] + ], + } + return { + "observed_at": datetime.now(timezone.utc).isoformat(), + "version": VERSION, + "source_commit": SOURCE, + "tag": f"v{VERSION}", + "tag_object": ref["sha"], + "release_url": release["html_url"], + "artifacts": artifacts, + "runs": runs, + } + + +def verify_trial_observation( + trial: dict, + inventory: dict, + root: Path, + version: str, + wheel_digest: str, + proofs: set[str], +) -> None: + """Cross-check normalized counters against their retained native reports. + + Hashes establish byte identity. They do not prove that a process executed; + review of the retained measurement code and provenance remains required. + """ + + def referenced(reference: dict) -> Path: + if ( + not isinstance(reference.get("path"), str) + or reference["path"] not in inventory + or inventory[reference["path"]]["sha256"] != reference.get("sha256") + ): + raise ValueError("observation reference is absent from verified inventory") + return root / reference["path"] + + observed = read_json(referenced(trial.get("observation", {}))) + if observed.get("schema_version") != "openadapt.observed-release-trial/v1": + raise ValueError("wrong observed trial schema") + for field in ("class", "task_id", "condition", "trial", "counters"): + if observed.get(field) != trial[field]: + raise ValueError(f"retained observation differs from trial {field}") + runtime = observed.get("runtime", {}) + if runtime.get("version") != version or runtime.get("wheel_sha256") != wheel_digest: + raise ValueError("observed runtime identity differs") + proof_reference = runtime.get("installed_distribution_proof", {}) + proof_path = referenced(proof_reference) + if proof_reference["sha256"] not in proofs: + proof = read_json(proof_path) + if ( + proof.get("schema_version") != "openadapt.installed-distribution-proof/v1" + or proof.get("distribution") != "openadapt-flow" + or proof.get("version") != version + or proof.get("all_members_match") is not True + or proof.get("installed_extra_files") != [] + ): + raise ValueError("installed distribution proof is incomplete or mismatched") + wheel_reference = proof.get("wheel", {}) + wheel_path = referenced(wheel_reference) + if wheel_reference.get("sha256") != wheel_digest: + raise ValueError("installed proof names another wheel") + if wheel_reference.get("size_bytes") != wheel_path.stat().st_size: + raise ValueError("installed proof wheel size differs") + members = proof.get("members", []) + indexed = {m["path"]: m for m in members} + if len(indexed) != len(members): + raise ValueError("duplicate installed member proof") + with zipfile.ZipFile(wheel_path) as archive: + names = [ + n + for n in archive.namelist() + if n.startswith("openadapt_flow/") and not n.endswith("/") + ] + if not names or len(names) != len(set(names)) or set(names) != set(indexed): + raise ValueError( + "installed member proof does not cover the exact wheel" + ) + for name in names: + raw = archive.read(name) + item = indexed[name] + if ( + item.get("size_bytes") != len(raw) + or item.get("wheel_sha256") != sha(raw) + or item.get("installed_sha256") != sha(raw) + ): + raise ValueError("installed member differs from candidate wheel") + proofs.add(proof_reference["sha256"]) + + reports = observed.get("reports", []) + if not reports or len({r["path"] for r in reports}) != len(reports): + raise ValueError("observed trial needs distinct retained native reports") + primary = [] + model_calls = 0 + for reference in reports: + report = read_json(referenced(reference)) + for field in ("success", "transaction_outcome", "model_calls"): + if field not in report or report[field] != reference.get(field): + raise ValueError( + f"retained native report differs from observed {field}" + ) + if not isinstance(report["success"], bool): + raise ValueError("native report success must be a boolean") + count = report["model_calls"] + if isinstance(count, bool) or not isinstance(count, int) or count < 0: + raise ValueError("native model calls must be observed nonnegative integers") + model_calls += count + if reference.get("role") == "primary": + primary.append(report) + if len(primary) != 1 or model_calls != trial["counters"]["model_call_count"]: + raise ValueError("primary report or model call total differs") + final = primary[0] + if trial["class"] == "uncertain_delivery": + expected = ( + not final["success"] + and final["transaction_outcome"] == "RECONCILIATION_REQUIRED" + ) + elif trial["class"] == "safe_halt": + expected = not final["success"] and final["transaction_outcome"] != "VERIFIED" + else: + expected = final["success"] and final["transaction_outcome"] == "VERIFIED" + if not expected: + raise ValueError("primary native outcome does not satisfy its measured class") + references = observed.get("verification_references", []) + if not references: + raise ValueError("observed trial has no retained verification references") + for reference in references: + referenced(reference) + + +def validate_trial_events(trial: dict) -> None: + required = { + "uncertain_delivery": ("reconciliation_required_count",), + "declared_attended": ( + "authenticated_bound_decision_count", + "live_target_revalidation_count", + ), + "governed_repair": ( + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "live_target_revalidation_count", + ), + } + if any(trial["counters"][field] != 1 for field in required.get(trial["class"], ())): + raise ValueError( + "each trial must satisfy its own required class events exactly once" + ) + + +def summarize( + manifest_path: Path, wheel_digest: str, *, version: str = VERSION +) -> tuple[dict, dict]: + root = manifest_path.resolve().parent + raw = manifest_path.read_bytes() + document = json.loads(raw) + if ( + document["schema_version"] != "openadapt.measured-release-evidence/v1" + or document["evidence_class"] != "remote-safe-synthetic" + ): + raise ValueError("wrong measured evidence schema or class") + if document["runtime"] != {"version": version, "wheel_sha256": wheel_digest}: + raise ValueError("measured runtime does not match the candidate wheel") + inventory = {} + for item in document["artifacts"]: + relative = Path(item["path"]) + path = (root / relative).resolve() + if ( + relative.is_absolute() + or ".." in relative.parts + or not path.is_relative_to(root) + or (root / relative).is_symlink() + ): + raise ValueError("evidence artifact path escapes its manifest") + data = path.read_bytes() + if len(data) != item["size_bytes"] or sha(data) != item["sha256"]: + raise ValueError(f"artifact bytes differ: {relative}") + if str(relative) in inventory: + raise ValueError("duplicate evidence artifact path") + inventory[str(relative)] = item + reference_counts = Counter( + reference["sha256"] + for trial in document["trials"] + for reference in {item["sha256"]: item for item in trial["artifacts"]}.values() + ) + groups = defaultdict(list) + seen = set() + for trial in document["trials"]: + key = (trial["class"], trial["task_id"], trial["condition"], trial["trial"]) + if ( + key in seen + or trial["class"] not in trust.CAMPAIGN_CLASSES + or trial["runtime_version"] != version + ): + raise ValueError("duplicate, unknown-class, or wrong-runtime trial") + seen.add(key) + if not trial["artifacts"] or not any( + reference_counts[reference["sha256"]] == 1 + for reference in trial["artifacts"] + ): + raise ValueError("a measured trial must retain unique observed evidence") + for reference in trial["artifacts"]: + if ( + inventory.get(reference["path"], {}).get("sha256") + != reference["sha256"] + ): + raise ValueError("trial evidence is absent from the verified inventory") + if set(trial["counters"]) != COUNTERS: + raise ValueError( + "every trial must explicitly observe every outcome counter" + ) + if any( + isinstance(n, bool) or not isinstance(n, int) or n < 0 + for n in trial["counters"].values() + ): + raise ValueError("trial counters must be nonnegative integers") + groups[trial["class"]].append(trial) + missing = [name for name in trust.CAMPAIGN_CLASSES if not groups[name]] + if missing: + raise ValueError("measured trials are missing for " + ", ".join(missing)) + normalizer = document.get("normalizer", {}) + if ( + not isinstance(normalizer.get("path"), str) + or not normalizer["path"] + or normalizer["path"] not in inventory + or not isinstance(normalizer.get("sha256"), str) + or inventory[normalizer["path"]]["sha256"] != normalizer["sha256"] + ): + raise ValueError("measurement normalizer is absent from verified inventory") + proofs: set[str] = set() + for trial in document["trials"]: + validate_trial_events(trial) + verify_trial_observation(trial, inventory, root, version, wheel_digest, proofs) + summary = {} + for name in trust.CAMPAIGN_CLASSES: + trials = groups[name] + cells = Counter((t["task_id"], t["condition"]) for t in trials) + counts = {key: sum(t["counters"][key] for t in trials) for key in COUNTERS} + counts.update( + task_condition_cell_count=len(cells), + minimum_trials_per_cell=min(cells.values()), + observed_trial_count=len(trials), + ) + summary[name] = counts + trust.validate_campaign_summary(summary) + return summary, { + "manifest_sha256": sha(raw), + "artifact_count": len(inventory), + "trial_count": len(seen), + "task_count": len({t["task_id"] for t in document["trials"]}), + } + + +def observe_unpublished(wheel: Path, sdist: Path, source: str) -> dict: + if len(source) != 40 or any(c not in "0123456789abcdef" for c in source): + raise ValueError("candidate source must be an exact commit SHA") + with zipfile.ZipFile(wheel) as archive: + metadata_paths = [ + n for n in archive.namelist() if n.endswith(".dist-info/METADATA") + ] + if len(metadata_paths) != 1: + raise ValueError("wheel metadata is ambiguous") + wheel_metadata = BytesParser().parsebytes(archive.read(metadata_paths[0])) + with tarfile.open(sdist, "r:gz") as archive: + metadata_paths = [ + m + for m in archive.getmembers() + if m.name.count("/") == 1 and m.name.endswith("/PKG-INFO") + ] + if len(metadata_paths) != 1 or not metadata_paths[0].isfile(): + raise ValueError("sdist metadata is ambiguous") + stream = archive.extractfile(metadata_paths[0]) + assert stream is not None + sdist_metadata = BytesParser().parsebytes(stream.read()) + version = wheel_metadata["Version"] + if ( + wheel_metadata["Name"] != "openadapt-flow" + or sdist_metadata["Name"] != "openadapt-flow" + or sdist_metadata["Version"] != version + ): + raise ValueError("candidate package metadata differs") + if version != "1.35.1": + raise ValueError("this reviewed successor candidate must be version 1.35.1") + return { + "published": False, + "version": version, + "source_commit": source, + "observed_at": datetime.now(timezone.utc).isoformat(), + "required_external_gates": [ + "Review and merge the exact candidate", + "Run the protected-main full qualification and three-OS lifecycle", + "Publish and verify both immutable artifacts", + "Issue and project the exact signed admission", + ], + "artifacts": [ + { + "kind": kind, + "name": path.name, + "sha256": sha(path.read_bytes()), + "size_bytes": path.stat().st_size, + } + for kind, path in (("python-wheel", wheel), ("python-sdist", sdist)) + ], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--measured-manifest", type=Path) + parser.add_argument("--out", type=Path, required=True) + parser.add_argument("--unpublished-wheel", type=Path) + parser.add_argument("--unpublished-sdist", type=Path) + parser.add_argument("--source-commit") + args = parser.parse_args() + local_args = (args.unpublished_wheel, args.unpublished_sdist, args.source_commit) + if any(local_args) and not all(local_args): + parser.error( + "unpublished wheel, sdist, and source commit must be supplied together" + ) + observation = ( + observe_unpublished(*local_args) if all(local_args) else observe_release() + ) + wheel = next(a for a in observation["artifacts"] if a["kind"] == "python-wheel") + ledger = read_json(ROOT / "production-lifecycle-admissions.json") + previous = [] + for reference in ledger["admissions"]: + raw = (ROOT / reference["object_path"]).read_bytes() + if sha(raw) != reference["object_sha256"]: + raise ValueError("previous admission bytes differ from the ledger") + admission = json.loads(raw) + if admission["target"] == "flow": + previous.append(admission) + if not previous: + raise ValueError("previous Flow admission is absent") + last = max(previous, key=lambda a: a["release_identity"]["sequence"]) + candidate = { + "schema_version": "openadapt.unsigned-measured-release-candidate/v1", + "target": "flow", + "evidence_class": "remote-safe-synthetic", + "state": "evidence-incomplete", + "admission_issued": False, + "proposed_release_identity": { + "schema_version": "openadapt.monotonic-production-release/v1", + "channel": "production", + "sequence": last["release_identity"]["sequence"] + 1, + "previous_admission_sha256": last["admission_id_sha256"], + }, + "canonical_policy_commit": subprocess.check_output( + ["git", "rev-parse", "origin/main"], cwd=ROOT, text=True + ).strip(), + "scope": "Executed local synthetic SQLite and pixel fixture; synthetic reviewer inputs; no customer qualification or actual human approval.", + "release_observation": observation, + "required_campaign_classes": list(trust.CAMPAIGN_CLASSES), + } + if args.measured_manifest: + candidate["measured_manifest_sha256"] = sha(args.measured_manifest.read_bytes()) + measured_document = read_json(args.measured_manifest) + candidate["measured_scope"] = measured_document.get("scope") + candidate["limitations"] = measured_document.get("limitations", []) + try: + summary, measured = summarize( + args.measured_manifest, + wheel["sha256"], + version=observation.get("version", VERSION), + ) + except (ValueError, trust.TrustError) as exc: + candidate["validation_errors"] = [str(exc)] + else: + if measured_document.get("candidate_ready") is not True: + candidate["validation_errors"] = [ + "measured evidence declares unresolved admission requirements" + ] + elif not candidate["measured_scope"]: + candidate["validation_errors"] = ["measured scope must be explicit"] + else: + candidate.update( + state="ready-for-release-review" + if observation.get("published") is False + else "ready-for-review", + campaign_summary=summary, + measured_evidence=measured, + ) + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text(json.dumps(candidate, indent=2, sort_keys=True) + "\n") + print( + json.dumps( + { + "state": candidate["state"], + "admission_issued": False, + "candidate": str(args.out), + } + ) + ) + return 1 if candidate.get("validation_errors") else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/local-candidates/flow-1.35.1-measured/README.md b/local-candidates/flow-1.35.1-measured/README.md new file mode 100644 index 0000000..870afcb --- /dev/null +++ b/local-candidates/flow-1.35.1-measured/README.md @@ -0,0 +1,41 @@ +# Flow 1.35.1 measured release candidate + +This unsigned candidate passes the six-class evidence checks for the exact +local wheel. It is ready for release review. Publication and admission remain +false. + +The source commit is `aed32758b7342c61787a3b56fe940e1e9f2d648a`. The retained +manifest digest is +`sha256:2cd3b47a9c1fc3d81b1207e7f64441d82ad7f0f62babf49d8ffde6ae2ac2c7ca`. +The verifier checks all 1,617 inventoried artifacts and 36 selected trial groups. + +| Class | Selected groups | Required outcome | +| --- | ---: | --- | +| Healthy | 6 | Verified effect | +| Safe halt | 15 | Refusal before an unsafe effect | +| Uncertain delivery | 6 | Reconciliation required, without retry or replay | +| Idempotency replay | 3 | One verified write, then same-key refusal with unchanged input and effect records | +| Declared attended | 3 | Bound synthetic decision, live revalidation, and verified final effect | +| Governed repair | 3 | Reviewed and approved candidate, complete campaigns, retained proof, and verified canary | + +Each of the 12 selected task-condition cells has three groups. The 42 complete +raw groups retain 54 native phase reports. The six excluded groups are the three +moderate-display trials, whose declared contract permits a verified effect or a +safe halt, and three retained-write timeout trials, whose complete effect proof +permits `VERIFIED`. The selected uncertain-delivery class requires +`RECONCILIATION_REQUIRED`. Two incomplete repair instrumentation/restart attempts +remain retained and excluded. Classes are distinct, but outcome indicators can +overlap across phases; these counts don't define an overall success percentage. + +The scope is synthetic pixels, local SQLite, and synthetic reviewer inputs. +The actual repair refreshes the typed field's OCR anchor at step 002. The +hidden-Save halt is a separate refusal check. No customer qualification or +actual human approval is established. Compact scaled identity regions and the +excluded moderate-display identity-tokenization case can still refuse. Their +exact evidence remains in the manifest; no threshold was relaxed. + +The source, wheel, sdist, and measured evidence require review before the +protected-main qualification and publication steps. A changed published wheel +requires new exact-byte evidence. The existing issuer must then bind the +reviewed release and workflow contracts before any signed admission or live +projection changes. The published Flow admission still names 1.34.0. diff --git a/local-candidates/flow-1.35.1-measured/candidate.json b/local-candidates/flow-1.35.1-measured/candidate.json new file mode 100644 index 0000000..8e2261f --- /dev/null +++ b/local-candidates/flow-1.35.1-measured/candidate.json @@ -0,0 +1,203 @@ +{ + "admission_issued": false, + "campaign_summary": { + "declared_attended": { + "approved_repair_count": 0, + "authenticated_bound_decision_count": 3, + "blind_retry_count": 0, + "live_target_revalidation_count": 3, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 3, + "over_halt_count": 0, + "policy_approved_repair_count": 0, + "reconciliation_required_count": 0, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 0, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 1, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + }, + "governed_repair": { + "approved_repair_count": 3, + "authenticated_bound_decision_count": 0, + "blind_retry_count": 0, + "live_target_revalidation_count": 3, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 3, + "over_halt_count": 0, + "policy_approved_repair_count": 3, + "reconciliation_required_count": 3, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 3, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 1, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + }, + "healthy": { + "approved_repair_count": 0, + "authenticated_bound_decision_count": 0, + "blind_retry_count": 0, + "live_target_revalidation_count": 0, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 6, + "over_halt_count": 0, + "policy_approved_repair_count": 0, + "reconciliation_required_count": 0, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 0, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 2, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + }, + "idempotency_replay": { + "approved_repair_count": 0, + "authenticated_bound_decision_count": 0, + "blind_retry_count": 0, + "live_target_revalidation_count": 0, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 3, + "over_halt_count": 0, + "policy_approved_repair_count": 0, + "reconciliation_required_count": 0, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 0, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 1, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + }, + "safe_halt": { + "approved_repair_count": 0, + "authenticated_bound_decision_count": 0, + "blind_retry_count": 0, + "live_target_revalidation_count": 0, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 15, + "over_halt_count": 0, + "policy_approved_repair_count": 0, + "reconciliation_required_count": 12, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 0, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 5, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + }, + "uncertain_delivery": { + "approved_repair_count": 0, + "authenticated_bound_decision_count": 0, + "blind_retry_count": 0, + "live_target_revalidation_count": 0, + "minimum_trials_per_cell": 3, + "model_call_count": 0, + "observed_trial_count": 6, + "over_halt_count": 0, + "policy_approved_repair_count": 0, + "reconciliation_required_count": 6, + "replay_dispatch_count": 0, + "retained_repair_evidence_count": 0, + "silent_incorrect_success_count": 0, + "task_condition_cell_count": 2, + "unplanned_intervention_count": 0, + "unsafe_effect_count": 0, + "unverified_direct_action_count": 0 + } + }, + "canonical_policy_commit": "9b61391713203998e584f4834e4812bb41b40f6e", + "evidence_class": "remote-safe-synthetic", + "limitations": [ + { + "id": "compact-scale-safe-refusal", + "observed": "The actual runtime refuses unreadable identity at this compact scale. The corrected campaign must match that refusal. This candidate does not establish broad scale support.", + "scope": "Compact synthetic identifier at 1.15x display scale", + "verification_artifacts": [ + "source-fix-repros/repair-independent-review/sampler-runtime-parity.json", + "source-fix-repros/repair-independent-review/sampler_runtime_parity.py", + "source-fix-repros/repair-independent-review/retention-provenance.json" + ] + }, + { + "follow_up": "Investigate identity tokenization sensitivity to a one-pixel resolved target shift while preserving exact identity and ambiguity refusal requirements.", + "id": "moderate-display-identity-tokenization-refusal", + "interpretation": "The reports establish one-pixel resolution/identity-tokenization sensitivity after the OCR enumeration change. This is not a target miss. No counterfactual execution or tuning experiment was run, so this record does not isolate every internal causal contribution.", + "observed": "All three old trials verify. All three candidate trials refuse identity before Save. Both releases find Save at OCR confidence 1.0 in identical before frames. The resolved x coordinate changes from 1060 to 1061. The candidate identity OCR joins Cardiology and selected, and coverage changes from 1.0 to 0.2381.", + "qualification_accounting": "The predeclared condition permits VERIFIED or a safe halt. All three candidate trials meet that alternative but remain excluded from the six-class selected trial count. No overall parity or improvement claim follows.", + "scope": "The three excluded moderate_display_drift trials compared with frozen 1.35.0", + "verification_artifacts": [ + "limitations/moderate-display-identity-tokenization/comparison.json" + ] + } + ], + "measured_evidence": { + "artifact_count": 1617, + "manifest_sha256": "sha256:2cd3b47a9c1fc3d81b1207e7f64441d82ad7f0f62babf49d8ffde6ae2ac2c7ca", + "task_count": 3, + "trial_count": 36 + }, + "measured_manifest_sha256": "sha256:2cd3b47a9c1fc3d81b1207e7f64441d82ad7f0f62babf49d8ffde6ae2ac2c7ca", + "measured_scope": { + "application": "Pillow synthetic clinic fixture", + "operator_principal": "Explicit synthetic local protocol inputs; no real human/authentication provider proof", + "promoted_repair": "Retained typed-field OCR anchor refresh at step_002. The hidden-Save halt is a separate expected halt check; the promoted anchor does not fix or unhide Save. Discovery and canary use distinct reset SQLite oracles.", + "required_path": "Default real attended and encrypted repair paths; added deployment adapters/plaintext diagnostics do not substitute for those paths", + "runtime": "Exact installed unmodified candidate wheel; identity binds the version and digest", + "system_of_record": "Local SQLite" + }, + "proposed_release_identity": { + "channel": "production", + "previous_admission_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", + "schema_version": "openadapt.monotonic-production-release/v1", + "sequence": 2 + }, + "release_observation": { + "artifacts": [ + { + "kind": "python-wheel", + "name": "openadapt_flow-1.35.1-py3-none-any.whl", + "sha256": "sha256:3e0d3c0fc423b6055d1972a2c8f90d9a5622330a0888f2cfaaeb7993ff23cb99", + "size_bytes": 2129387 + }, + { + "kind": "python-sdist", + "name": "openadapt_flow-1.35.1.tar.gz", + "sha256": "sha256:e84ccd8e7dd32ad074d5becb351d806e7a735fa1ef5e6ae95c9d4bb48f621588", + "size_bytes": 21133468 + } + ], + "observed_at": "2026-09-08T22:35:52.508027+00:00", + "published": false, + "required_external_gates": [ + "Review and merge the exact candidate", + "Run the protected-main full qualification and three-OS lifecycle", + "Publish and verify both immutable artifacts", + "Issue and project the exact signed admission" + ], + "source_commit": "aed32758b7342c61787a3b56fe940e1e9f2d648a", + "version": "1.35.1" + }, + "required_campaign_classes": [ + "healthy", + "safe_halt", + "idempotency_replay", + "uncertain_delivery", + "declared_attended", + "governed_repair" + ], + "schema_version": "openadapt.unsigned-measured-release-candidate/v1", + "scope": "Executed local synthetic SQLite and pixel fixture; synthetic reviewer inputs; no customer qualification or actual human approval.", + "state": "ready-for-release-review", + "target": "flow" +} diff --git a/tests/test_measured_flow_candidate.py b/tests/test_measured_flow_candidate.py new file mode 100644 index 0000000..0e7e5b3 --- /dev/null +++ b/tests/test_measured_flow_candidate.py @@ -0,0 +1,509 @@ +"""The review candidate binds counted trials to exact retained bytes.""" + +import importlib.util +import json +import io +import tarfile +import zipfile +from pathlib import Path +import tempfile +import unittest +from unittest import mock + +_PATH = ( + Path(__file__).resolve().parents[1] + / "local-candidates/flow-1.35.0-measured/prepare.py" +) +_SPEC = importlib.util.spec_from_file_location("measured_flow_candidate", _PATH) +assert _SPEC and _SPEC.loader +candidate = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(candidate) +WHEEL = "sha256:" + "a" * 64 + + +def manifest(tmp_path, *, wheel=None, version="1.35.0"): + if wheel is None: + wheel, _ = archives(tmp_path, version, version) + digest = candidate.sha(wheel.read_bytes()) + artifacts = [] + + def retain(path): + reference = { + "path": path.name, + "sha256": candidate.sha(path.read_bytes()), + "size_bytes": path.stat().st_size, + } + artifacts.append(reference) + return reference + + wheel_reference = retain(wheel) + with zipfile.ZipFile(wheel) as archive: + members = [ + { + "path": name, + "size_bytes": len(archive.read(name)), + "wheel_sha256": candidate.sha(archive.read(name)), + "installed_sha256": candidate.sha(archive.read(name)), + } + for name in archive.namelist() + if name.startswith("openadapt_flow/") + ] + proof_path = tmp_path / "installed.json" + proof_path.write_text( + json.dumps( + { + "schema_version": "openadapt.installed-distribution-proof/v1", + "distribution": "openadapt-flow", + "version": version, + "wheel": wheel_reference, + "members": members, + "installed_extra_files": [], + "all_members_match": True, + } + ) + ) + proof = retain(proof_path) + normalizer_path = tmp_path / "normalizer.py" + normalizer_path.write_text("# Explicit parser fixture; no execution claim\n") + normalizer = retain(normalizer_path) + trials = [] + for name in candidate.trust.CAMPAIGN_CLASSES: + counters = dict.fromkeys(candidate.COUNTERS, 0) + if name == "uncertain_delivery": + counters["reconciliation_required_count"] = 1 + if name == "declared_attended": + counters["authenticated_bound_decision_count"] = 1 + counters["live_target_revalidation_count"] = 1 + if name == "governed_repair": + for field in ( + "policy_approved_repair_count", + "approved_repair_count", + "retained_repair_evidence_count", + "live_target_revalidation_count", + ): + counters[field] = 1 + for trial in range(3): + success = name not in {"safe_halt", "uncertain_delivery"} + report = { + "fixture": True, + "trial_identity": [name, trial], + "success": success, + "transaction_outcome": "VERIFIED" + if success + else "RECONCILIATION_REQUIRED", + "model_calls": 0, + } + report_path = tmp_path / f"{name}-{trial}-report.json" + report_path.write_text(json.dumps(report)) + report_reference = retain(report_path) + observed = { + "schema_version": "openadapt.observed-release-trial/v1", + "class": name, + "task_id": "test-fixture", + "condition": "one", + "trial": trial, + "counters": counters.copy(), + "runtime": { + "version": version, + "wheel_sha256": digest, + "installed_distribution_proof": proof, + }, + "reports": [ + { + **report_reference, + "role": "primary", + **{ + k: report[k] + for k in ("success", "transaction_outcome", "model_calls") + }, + } + ], + "verification_references": [ + {**report_reference, "role": "explicit-parser-fixture"} + ], + } + path = tmp_path / f"{name}-{trial}.json" + path.write_text(json.dumps(observed)) + reference = retain(path) + trials.append( + { + "class": name, + "task_id": "test-fixture", + "condition": "one", + "trial": trial, + "runtime_version": version, + "observation": reference, + "artifacts": [reference, report_reference], + "counters": counters.copy(), + } + ) + value = { + "schema_version": "openadapt.measured-release-evidence/v1", + "evidence_class": "remote-safe-synthetic", + "runtime": {"version": version, "wheel_sha256": digest}, + "normalizer": normalizer, + "artifacts": artifacts, + "trials": trials, + } + path = tmp_path / "manifest.json" + path.write_text(json.dumps(value)) + return (path, value) + + +def archives(tmp_path, wheel_version="1.35.1", sdist_version="1.35.1"): + wheel = tmp_path / "candidate.whl" + sdist = tmp_path / "candidate.tar.gz" + with zipfile.ZipFile(wheel, "w") as output: + output.writestr( + "openadapt_flow/__init__.py", f'__version__ = "{wheel_version}"\n' + ) + output.writestr( + "openadapt_flow.dist-info/METADATA", + f"Name: openadapt-flow\nVersion: {wheel_version}\n", + ) + metadata = f"Name: openadapt-flow\nVersion: {sdist_version}\n".encode() + with tarfile.open(sdist, "w:gz") as output: + member = tarfile.TarInfo("openadapt_flow/PKG-INFO") + member.size = len(metadata) + output.addfile(member, io.BytesIO(metadata)) + return (wheel, sdist) + + +def update_references(value, path): + """Rehash a deliberately modified fixture without hiding semantic changes.""" + digest = candidate.sha(path.read_bytes()) + + def walk(node): + if isinstance(node, dict): + if node.get("path") == path.name: + node["sha256"] = digest + if "size_bytes" in node: + node["size_bytes"] = path.stat().st_size + for child in node.values(): + walk(child) + elif isinstance(node, list): + for child in node: + walk(child) + + walk(value) + + +class MeasuredFlowCandidateTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.tmp_path = Path(temporary.name) + + def test_counts_derive_from_unique_observed_trials(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + summary, evidence = candidate.summarize(path, value["runtime"]["wheel_sha256"]) + assert evidence["trial_count"] == 18 + assert evidence["manifest_sha256"] == candidate.sha(path.read_bytes()) + assert summary["governed_repair"]["approved_repair_count"] == 3 + assert summary["healthy"]["minimum_trials_per_cell"] == 3 + + def _check_incomplete_or_unsafe_evidence_refuses(self, mutation): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + wheel_digest = value["runtime"]["wheel_sha256"] + mutation(value) + path.write_text(json.dumps(value)) + with self.assertRaises((ValueError, candidate.trust.TrustError)): + candidate.summarize(path, wheel_digest) + + def test_incomplete_or_unsafe_evidence_refuses_0(self): + self._check_incomplete_or_unsafe_evidence_refuses(lambda m: m["trials"].pop()) + + def test_incomplete_or_unsafe_evidence_refuses_1(self): + self._check_incomplete_or_unsafe_evidence_refuses( + lambda m: m["trials"].append(m["trials"][0]) + ) + + def test_incomplete_or_unsafe_evidence_refuses_2(self): + self._check_incomplete_or_unsafe_evidence_refuses( + lambda m: m["trials"][0]["counters"].pop("silent_incorrect_success_count") + ) + + def test_incomplete_or_unsafe_evidence_refuses_3(self): + self._check_incomplete_or_unsafe_evidence_refuses( + lambda m: m["trials"][0]["counters"].update( + silent_incorrect_success_count=1 + ) + ) + + def test_incomplete_or_unsafe_evidence_refuses_4(self): + self._check_incomplete_or_unsafe_evidence_refuses( + lambda m: m["runtime"].update(wheel_sha256="sha256:" + "b" * 64) + ) + + def test_incomplete_or_unsafe_evidence_refuses_5(self): + self._check_incomplete_or_unsafe_evidence_refuses( + lambda m: m["trials"][0]["artifacts"].clear() + ) + + def test_changed_artifact_bytes_refuse(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + (tmp_path / "healthy-0.json").write_text("changed") + with self.assertRaisesRegex(ValueError, "artifact bytes differ"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_reused_observation_cannot_count_as_another_trial(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + value["trials"][1]["artifacts"] = value["trials"][0]["artifacts"] + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, "unique observed evidence"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_incomplete_input_writes_refusal_candidate(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + value["scope"] = "Explicit test fixture; no execution claim" + value["limitations"] = ["Two classes were not measured"] + value["trials"] = [ + t + for t in value["trials"] + if t["class"] not in {"declared_attended", "governed_repair"} + ] + path.write_text(json.dumps(value)) + self.enterContext( + mock.patch.object( + candidate, + "observe_release", + lambda: { + "artifacts": [ + { + "kind": "python-wheel", + "sha256": value["runtime"]["wheel_sha256"], + } + ] + }, + ) + ) + output = tmp_path / "candidate.json" + self.enterContext( + mock.patch.object( + candidate.sys, + "argv", + ["prepare.py", "--measured-manifest", str(path), "--out", str(output)], + ) + ) + assert candidate.main() == 1 + result = json.loads(output.read_text()) + assert result["state"] == "evidence-incomplete" + assert result["admission_issued"] is False + assert "declared_attended, governed_repair" in result["validation_errors"][0] + assert result["limitations"] == value["limitations"] + + def _check_wrong_unpublished_package_versions_refuse(self, versions): + tmp_path = self.tmp_path + wheel, sdist = archives(tmp_path, *versions) + with self.assertRaises(ValueError): + candidate.observe_unpublished(wheel, sdist, "c" * 40) + + def test_wrong_unpublished_package_versions_refuse_0(self): + self._check_wrong_unpublished_package_versions_refuse(("1.35.1", "1.35.0")) + + def test_wrong_unpublished_package_versions_refuse_1(self): + self._check_wrong_unpublished_package_versions_refuse(("1.35.0", "1.35.0")) + + def test_unpublished_candidate_cannot_assert_admission_or_publication(self): + tmp_path = self.tmp_path + wheel, sdist = archives(tmp_path) + path, value = manifest(tmp_path, wheel=wheel, version="1.35.1") + value.update( + candidate_ready=True, scope="Explicit test fixture; no execution claim" + ) + value["runtime"] = { + "version": "1.35.1", + "wheel_sha256": candidate.sha(wheel.read_bytes()), + } + for trial in value["trials"]: + trial["runtime_version"] = "1.35.1" + path.write_text(json.dumps(value)) + output = tmp_path / "candidate.json" + self.enterContext( + mock.patch.object( + candidate.sys, + "argv", + [ + "prepare.py", + "--measured-manifest", + str(path), + "--out", + str(output), + "--unpublished-wheel", + str(wheel), + "--unpublished-sdist", + str(sdist), + "--source-commit", + "c" * 40, + ], + ) + ) + assert candidate.main() == 0 + result = json.loads(output.read_text()) + assert result["state"] == "ready-for-release-review" + assert result["admission_issued"] is False + assert result["release_observation"]["published"] is False + assert result["release_observation"]["source_commit"] == "c" * 40 + assert len(result["release_observation"]["required_external_gates"]) == 4 + assert result["measured_evidence"]["trial_count"] == 18 + + def test_partial_unpublished_inputs_refuse_before_network_observation(self): + tmp_path = self.tmp_path + self.enterContext( + mock.patch.object( + candidate.sys, + "argv", + [ + "prepare.py", + "--out", + str(tmp_path / "out.json"), + "--unpublished-wheel", + str(tmp_path / "candidate.whl"), + ], + ) + ) + self.enterContext( + mock.patch.object( + candidate, + "observe_release", + lambda: self.fail("unexpected network request"), + ) + ) + with self.assertRaises(SystemExit) as error: + candidate.main() + assert error.exception.code == 2 + + def test_rehashed_observation_cannot_relabel_a_failed_trial(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + observed_path = tmp_path / "healthy-0.json" + observed = json.loads(observed_path.read_text()) + observed["class"] = "safe_halt" + observed["counters"]["unsafe_effect_count"] = 1 + observed_path.write_text(json.dumps(observed)) + update_references(value, observed_path) + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, "retained observation differs"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def _check_class_events_cannot_be_concentrated_in_one_trial( + self, class_name, field + ): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + selected = [trial for trial in value["trials"] if trial["class"] == class_name] + for trial, count in zip(selected, [3, 0, 0]): + trial["counters"][field] = count + observed_path = tmp_path / trial["observation"]["path"] + observed = json.loads(observed_path.read_text()) + observed["counters"][field] = count + observed_path.write_text(json.dumps(observed)) + update_references(value, observed_path) + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, "each trial must satisfy"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_class_events_cannot_be_concentrated_in_one_trial_0(self): + self._check_class_events_cannot_be_concentrated_in_one_trial( + "uncertain_delivery", "reconciliation_required_count" + ) + + def test_class_events_cannot_be_concentrated_in_one_trial_1(self): + self._check_class_events_cannot_be_concentrated_in_one_trial( + "declared_attended", "authenticated_bound_decision_count" + ) + + def test_class_events_cannot_be_concentrated_in_one_trial_2(self): + self._check_class_events_cannot_be_concentrated_in_one_trial( + "governed_repair", "approved_repair_count" + ) + + def test_attended_decision_completion_cannot_replace_native_verified_effect(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + report_path = tmp_path / "declared_attended-0-report.json" + report = json.loads(report_path.read_text()) + report.update( + success=False, + transaction_outcome="COMPLETED_UNVERIFIED", + report_success=True, + ) + report_path.write_text(json.dumps(report)) + update_references(value, report_path) + observed_path = tmp_path / "declared_attended-0.json" + observed = json.loads(observed_path.read_text()) + update_references(observed, report_path) + observed["reports"][0].update( + success=False, transaction_outcome="COMPLETED_UNVERIFIED" + ) + observed_path.write_text(json.dumps(observed)) + update_references(value, observed_path) + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, "primary native outcome"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_rehashed_native_report_must_match_normalized_claim(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + report_path = tmp_path / "healthy-0-report.json" + report = json.loads(report_path.read_text()) + report["model_calls"] = 9 + report_path.write_text(json.dumps(report)) + update_references(value, report_path) + observed_path = tmp_path / "healthy-0.json" + observed = json.loads(observed_path.read_text()) + update_references(observed, report_path) + observed_path.write_text(json.dumps(observed)) + update_references(value, observed_path) + path.write_text(json.dumps(value)) + with self.assertRaisesRegex( + ValueError, "native report differs from observed model_calls" + ): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_installed_proof_cannot_omit_a_wheel_member(self): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + proof_path = tmp_path / "installed.json" + proof = json.loads(proof_path.read_text()) + proof["members"] = [] + proof_path.write_text(json.dumps(proof)) + update_references(value, proof_path) + for trial in value["trials"]: + observed_path = tmp_path / trial["observation"]["path"] + observed = json.loads(observed_path.read_text()) + update_references(observed, proof_path) + observed_path.write_text(json.dumps(observed)) + update_references(value, observed_path) + path.write_text(json.dumps(value)) + with self.assertRaisesRegex( + ValueError, "installed member proof does not cover" + ): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def _check_missing_or_empty_normalizer_reference_refuses(self, reference): + tmp_path = self.tmp_path + path, value = manifest(tmp_path) + if reference is None: + value.pop("normalizer") + else: + value["normalizer"] = reference + path.write_text(json.dumps(value)) + with self.assertRaisesRegex(ValueError, "measurement normalizer is absent"): + candidate.summarize(path, value["runtime"]["wheel_sha256"]) + + def test_missing_or_empty_normalizer_reference_refuses_0(self): + self._check_missing_or_empty_normalizer_reference_refuses(None) + + def test_missing_or_empty_normalizer_reference_refuses_1(self): + self._check_missing_or_empty_normalizer_reference_refuses({}) + + def test_missing_or_empty_normalizer_reference_refuses_2(self): + self._check_missing_or_empty_normalizer_reference_refuses( + {"path": "", "sha256": ""} + )