Files
TheTree_mock/tools/check_p24_operations_evidence.py
T

748 lines
36 KiB
Python

#!/usr/bin/env python3
"""P24-02 Operations and Notification Acceptance Evidence Validator.
Strict verify-only validator enforcing schema, scope source manifest,
concrete condition-specific evidence artifacts, OS execution receipts,
cross-process restart proof, and all 8 required conditions:
- placement_and_invalid_drop
- modal_and_pause_resume
- unit_bubbles
- event_log
- important_warning_cutin
- click_alternatives
- japanese_input_and_scale
- final_validation_review
"""
import argparse
import hashlib
import json
import os
import re
import sys
from pathlib import Path
REQUIRED_CONDITIONS = [
"placement_and_invalid_drop",
"modal_and_pause_resume",
"unit_bubbles",
"event_log",
"important_warning_cutin",
"click_alternatives",
"japanese_input_and_scale",
"final_validation_review",
]
REQUIRED_SCOPE_FILES = [
"scenes/main/main.gd",
"scenes/main/world_preview.gd",
"scripts/ui/event_log_dialog.gd",
"scripts/ui/manual_save_slots_dialog.gd",
"scripts/ui/operation_feedback.gd",
"scripts/ui/operation_feedback.gd.uid",
"scripts/ui/operations_camera_controls.gd",
"scripts/ui/operations_camera_controls.gd.uid",
"tests/ui/test_operations_notification.gd",
"tests/ui/test_operations_notification.gd.uid",
"tests/ui/fixtures/operations_os_observer.gd",
"tests/ui/fixtures/operations_os_observer.gd.uid",
"tools/check_p24_operations_evidence.py",
"tools/run_all_tests.py",
"tools/regression/suite-policy.json",
"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json",
]
EXPECTED_TASK_ID = "P24-02"
EXPECTED_BASE_COMMIT = "1a5abc65163097d4b36a060d68f58c92093eb4e9"
EXPECTED_SPEC_SHA256 = "738268b406e381ccab415b426d4a547b02e7cb2a481cefce65579a1e346834c7"
HEX64_PATTERN = re.compile(r"^[0-9a-f]{64}$")
HEX_COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$|^[0-9a-f]{64}$")
JAPANESE_CHAR_PATTERN = re.compile(r"[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]")
MAX_VALID_PID = 4_194_304
def compute_sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def get_actual_git_head(root: Path = None) -> str:
root = root if root is not None else Path(__file__).resolve().parents[1]
try:
import subprocess
res = subprocess.run(["git", "rev-parse", "HEAD"], cwd=root, capture_output=True, text=True, timeout=5)
if res.returncode == 0:
h = res.stdout.strip().lower()
if HEX_COMMIT_PATTERN.match(h) and not (set(h) <= {"0"} or set(h) <= {"f"}):
for name in ("CANDIDATE_COMMIT", "TARGET_HEAD"):
if name in os.environ and os.environ[name].strip().lower() != h:
print(f"ERROR: {name} mismatch with real git HEAD {h}", file=sys.stderr)
return ""
return h
except Exception:
pass
return ""
def get_required_suite_roster(root: Path) -> list[str]:
import importlib.util
spec = importlib.util.spec_from_file_location("p24_required_roster", root / "tools/run_all_tests.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module.roster(root)
def find_index_file(custom_path: str = None) -> Path:
if custom_path:
p = Path(custom_path)
if p.is_file():
return p.resolve()
if p.is_dir():
candidate = p / "index.json"
if candidate.is_file():
return candidate.resolve()
candidate2 = p / "p24-operations-evidence" / "index.json"
if candidate2.is_file():
return candidate2.resolve()
build_dir = os.environ.get("BUILD_OUTPUT_DIR")
if build_dir:
candidate = Path(build_dir) / "p24-operations-evidence" / "index.json"
if candidate.is_file():
return candidate.resolve()
candidate2 = Path(build_dir) / "index.json"
if candidate2.is_file():
return candidate2.resolve()
candidates = [
Path(".runtime/P24-02/build/p24-operations-evidence/index.json"),
Path("docs/plans/original-completion-2026-10-02/execution/p24-02/evidence/index.json"),
Path("artifacts/p24-02-operations/index.json"),
]
for c in candidates:
if c.is_file():
return c.resolve()
return None
def _is_bare_or_dummy_artifact(data: dict) -> bool:
if not isinstance(data, dict):
return True
if len(data) <= 1:
return True
trivial_keys = {"pid", "status", "passed", "id", "timestamp", "name", "contract_id"}
if set(data.keys()).issubset(trivial_keys):
return True
return False
def _validate_condition_artifact_content(
cond: str,
art_data: dict,
art_type: str,
evidence_dir: Path,
root_data: dict,
validated_artifacts: dict,
) -> tuple[bool, str]:
if not isinstance(art_data, dict):
return False, f"Artifact content for condition '{cond}' must be a JSON object"
if _is_bare_or_dummy_artifact(art_data):
return False, f"Artifact for condition '{cond}' is a bare self-declaration/PID-only JSON without substantive evidence details"
if cond == "placement_and_invalid_drop":
if "grid_snap" not in art_data:
return False, "Missing 'grid_snap' in placement evidence"
if "connection_warning" not in art_data:
return False, "Missing 'connection_warning' in placement evidence"
if not art_data.get("valid_placement"):
return False, "Missing 'valid_placement' in placement evidence"
if not art_data.get("invalid_placement_rejection"):
return False, "Missing 'invalid_placement_rejection' in placement evidence"
if not art_data.get("click_assignment"):
return False, "Missing 'click_assignment' in placement evidence"
elif cond == "modal_and_pause_resume":
if not art_data.get("modal_input_blocked"):
return False, "Missing 'modal_input_blocked': true in modal/pause evidence"
if not art_data.get("behind_dialog_suppressed") and not (
art_data.get("behind_dialog_build_suppressed") and art_data.get("behind_dialog_drop_suppressed")
):
return False, "Missing behind-dialog input suppression verification in modal evidence"
if not art_data.get("clock_pause"):
return False, "Missing 'clock_pause' (freezing simulation on real delta) in modal/pause evidence"
if not art_data.get("clock_resume"):
return False, "Missing 'clock_resume' (advancing exactly once on real delta) in modal/pause evidence"
if not art_data.get("save_identity_preserved"):
return False, "Missing 'save_identity_preserved': true in modal/pause evidence"
# Separate process restart verification (別process restart・保存再開とPID)
cross_proc = art_data.get("cross_process", art_data.get("process_restart", root_data.get("cross_process")))
if not cross_proc or not isinstance(cross_proc, dict):
return False, "Missing verified separate process restart evidence ('cross_process' or 'process_restart' with distinct producer and consumer PIDs)"
producer_pid = cross_proc.get("producer_pid")
consumer_pid = cross_proc.get("consumer_pid")
# Support artifact-linked cross-process structure (P24-03 pattern)
if (producer_pid is None or consumer_pid is None) and "producer" in cross_proc and "consumer" in cross_proc:
prod_art_id = cross_proc["producer"]
cons_art_id = cross_proc["consumer"]
if prod_art_id in validated_artifacts and cons_art_id in validated_artifacts:
try:
prod_json = json.loads(validated_artifacts[prod_art_id]["file"].read_text(encoding="utf-8"))
cons_json = json.loads(validated_artifacts[cons_art_id]["file"].read_text(encoding="utf-8"))
producer_pid = prod_json.get("pid")
consumer_pid = cons_json.get("pid")
if prod_json.get("manifest_sha256") and cons_json.get("manifest_sha256"):
if prod_json["manifest_sha256"] != cons_json["manifest_sha256"]:
return False, "Cross-process restart manifest_sha256 mismatch between producer and consumer"
if prod_json.get("world_sha256") and cons_json.get("world_sha256"):
if prod_json["world_sha256"] != cons_json["world_sha256"]:
return False, "Cross-process restart world_sha256 mismatch between producer and consumer"
except Exception as e:
return False, f"Failed to parse cross-process producer/consumer artifacts: {e}"
if isinstance(producer_pid, bool) or not isinstance(producer_pid, int) or producer_pid <= 1:
return False, f"Invalid or missing cross-process producer_pid: {producer_pid!r}"
if isinstance(consumer_pid, bool) or not isinstance(consumer_pid, int) or consumer_pid <= 1:
return False, f"Invalid or missing cross-process consumer_pid: {consumer_pid!r}"
if producer_pid == consumer_pid:
return False, f"Cross-process restart requires distinct processes; producer and consumer share identical PID {producer_pid}"
state_preserved = (
cross_proc.get("same_manifest_world") is True
or cross_proc.get("whole_model_and_screen_values_equal") is True
or cross_proc.get("state_preserved") is True
or ("manifest_sha256" in cross_proc and "world_sha256" in cross_proc)
or ("producer" in cross_proc and "consumer" in cross_proc)
)
if not state_preserved:
return False, "Cross-process restart evidence missing proof of save/world consistency across distinct processes"
elif cond == "unit_bubbles":
if not art_data.get("zzz_bubble"):
return False, "Missing 'zzz_bubble' in unit bubbles evidence"
if not art_data.get("anger_bubble"):
return False, "Missing 'anger_bubble' in unit bubbles evidence"
if not art_data.get("heart_bubble"):
return False, "Missing 'heart_bubble' with authoritative relationship model in unit bubbles evidence"
if not art_data.get("stale_removal"):
return False, "Missing 'stale_removal' state transition clearing in unit bubbles evidence"
tracked = art_data.get("bubbles_tracked", art_data.get("bubble_events_count", 0))
if isinstance(tracked, bool) or not isinstance(tracked, int) or tracked < 3:
return False, f"Unit bubbles evidence must track at least 3 bubble events, got: {tracked!r}"
elif cond == "event_log":
limit = art_data.get("recent_limit")
if isinstance(limit, bool) or not isinstance(limit, int) or limit > 256 or limit <= 0:
return False, f"Event log recent limit must be positive int <= 256, got: {limit!r}"
if not art_data.get("readable_japanese"):
return False, "Missing 'readable_japanese': true in event log evidence"
if not art_data.get("deduplicated"):
return False, "Missing 'deduplicated': true in event log evidence"
events = art_data.get("events")
if not isinstance(events, list) or not events:
return False, "Event log evidence must include non-empty 'events' list"
for i, ev in enumerate(events):
if not isinstance(ev, dict):
return False, f"Event log row {i} must be a dictionary"
if not ev.get("event_id") or not ev.get("event_type"):
return False, f"Event log row {i} missing event_id or event_type"
title = ev.get("title", "")
if not isinstance(title, str) or not JAPANESE_CHAR_PATTERN.search(title):
return False, f"Event log row {i} title must contain readable Japanese, got: {title!r}"
elif cond == "important_warning_cutin":
if not art_data.get("defense_cutin"):
return False, "Missing 'defense_cutin' in cutin evidence"
if not art_data.get("shield_vanishing_cutin"):
return False, "Missing 'shield_vanishing_cutin' in cutin evidence"
if not art_data.get("simulation_stop") or not art_data.get("simulation_resume"):
return False, "Missing simulation stop/resume behavior in cutin evidence"
elif cond == "click_alternatives":
for k in ["zoom_in", "zoom_out", "pan", "reset", "grid_toggle", "click_assignment"]:
if not art_data.get(k):
return False, f"Missing '{k}' click alternative in click alternatives evidence"
if not art_data.get("presentation_only"):
return False, "Missing 'presentation_only': true (no model corruption) in click evidence"
elif cond == "japanese_input_and_scale":
# IME composition verification
ime_comp = art_data.get("ime_composition")
if not isinstance(ime_comp, dict):
return False, "Missing or invalid 'ime_composition' evidence object (must be JSON object with composition details)"
comp_text = ime_comp.get("text", ime_comp.get("composition_string", ime_comp.get("value", "")))
if not isinstance(comp_text, str) or not JAPANESE_CHAR_PATTERN.search(comp_text):
return False, f"'ime_composition' must contain Japanese text, got: {comp_text!r}"
cursor = ime_comp.get("cursor_position", ime_comp.get("cursor", ime_comp.get("position")))
if cursor is not None and (isinstance(cursor, bool) or not isinstance(cursor, int) or cursor < 0):
return False, f"'ime_composition' cursor position must be non-negative integer, got: {cursor!r}"
# IME confirm verification
ime_conf = art_data.get("ime_confirm")
if not isinstance(ime_conf, dict):
return False, "Missing or invalid 'ime_confirm' evidence object (must be JSON object with confirmation details)"
conf_text = ime_conf.get("text", ime_conf.get("confirmed_text", ime_conf.get("value", "")))
if not isinstance(conf_text, str) or not JAPANESE_CHAR_PATTERN.search(conf_text):
return False, f"'ime_confirm' must contain confirmed Japanese text, got: {conf_text!r}"
if not (ime_conf.get("applied") is True or ime_conf.get("confirmed") is True or ime_conf.get("status") in ("confirmed", "applied")):
return False, "Missing 'applied': true or 'confirmed': true in 'ime_confirm' evidence"
# IME cancel verification
ime_canc = art_data.get("ime_cancel")
if not isinstance(ime_canc, dict):
return False, "Missing or invalid 'ime_cancel' evidence object (must be JSON object with cancel details)"
if not (
ime_canc.get("canceled") is True
or ime_canc.get("reverted") is True
or ime_canc.get("cleared") is True
or ime_canc.get("status") in ("canceled", "reverted", "cleared")
):
return False, "Missing cancel/revert confirmation in 'ime_cancel' evidence"
# Cryptographic owned save slot path
path_str = art_data.get("cryptographic_owned_path")
if not path_str or not isinstance(path_str, str):
return False, f"Missing 'cryptographic_owned_path' in Japanese input evidence: {path_str!r}"
slot_filename = Path(path_str).name
if not re.match(r"^manual_ui_[0-9a-f]{32}$", slot_filename):
return False, f"Invalid cryptographic owned save slot filename (must match ^manual_ui_[0-9a-f]{{32}}$): {slot_filename!r}"
if not art_data.get("invalid_input_feedback"):
return False, "Missing 'invalid_input_feedback' in Japanese input evidence"
# Window scales inspection list
window_scales = art_data.get("window_scales")
if not isinstance(window_scales, list) or not window_scales:
return False, "Missing 'window_scales' inspection list in scale evidence"
for i, entry in enumerate(window_scales):
if not isinstance(entry, dict):
return False, f"window_scales entry {i} must be a dictionary"
ws = entry.get("window_size", entry.get("size"))
if ws is not None:
if not isinstance(ws, (list, tuple)) or len(ws) != 2:
return False, f"window_scales entry {i} window_size must be 2-element [w, h]"
ew, eh = ws
if isinstance(ew, bool) or isinstance(eh, bool) or not isinstance(ew, int) or not isinstance(eh, int) or ew <= 0 or eh <= 0:
return False, f"window_scales entry {i} dimensions must be positive integers: {ws!r}"
readability = entry.get("readability")
if readability is not True and (not isinstance(readability, str) or not readability.strip()):
return False, f"window_scales entry {i} lacks verified 'readability': true or legibility description"
clipping = entry.get("clipping")
if clipping is not False and clipping not in ("none", "no_clipping"):
return False, f"window_scales entry {i} has unexpected clipping: {clipping!r} (expected false or 'none')"
focus = entry.get("focus")
if focus is not True:
return False, f"window_scales entry {i} missing 'focus': true"
sc = entry.get("screenshot")
if sc:
if not isinstance(sc, str) or not sc.endswith(".png"):
return False, f"window_scales entry {i} screenshot must be a .png file: {sc!r}"
sc_path = evidence_dir / sc if evidence_dir else Path(sc)
if not sc_path.is_file() or sc_path.stat().st_size == 0:
return False, f"window_scales entry {i} screenshot missing or empty on disk: {sc!r}"
# Windows acceptance remains pending; macOS verified
if art_data.get("windows_acceptance") is True:
return False, "Premature claim of Windows acceptance; Windows release acceptance must remain separate pending condition"
elif cond == "final_validation_review":
suite = art_data.get("test_suite")
if suite != "tests/ui/test_operations_notification.gd":
return False, f"Unexpected test suite in final validation review: {suite!r}"
failures = art_data.get("failures")
if isinstance(failures, bool) or not isinstance(failures, int) or failures != 0:
return False, f"Test failures must be 0, got: {failures!r}"
checks = art_data.get("checks")
if isinstance(checks, bool) or not isinstance(checks, int) or checks <= 0:
return False, f"Test checks must be positive integer, got: {checks!r}"
# Full roster verification (全rosterの実スイート集合/ソース一致/結果を照合する)
full_roster = art_data.get("full_roster")
if not full_roster or not isinstance(full_roster, dict):
# Check if root index provides full_regression dict
full_roster = root_data.get("full_regression")
if not full_roster or not isinstance(full_roster, dict):
return False, "Missing 'full_roster' summary in final validation review evidence"
repo_root = Path(".").resolve()
required_suites = get_required_suite_roster(repo_root)
total_required = len(required_suites)
for field in ("total_suites", "suites_count"):
if field in full_roster and (type(full_roster[field]) is not int or full_roster[field] != total_required):
return False, f"Full roster {field} must equal {total_required}"
roster_failures = full_roster.get("failures")
if "failures" in full_roster and (type(roster_failures) is not int or roster_failures != 0):
return False, f"Full roster regression reported failures: {roster_failures!r}"
# Check suite entries if provided in full_roster or referenced all-tests artifact
suite_entries = full_roster.get("results", full_roster.get("rows", full_roster.get("all_tests")))
if isinstance(suite_entries, str) and suite_entries in validated_artifacts:
rows_art = validated_artifacts[suite_entries]
try:
suite_entries = json.loads(rows_art["file"].read_text(encoding="utf-8"))
except Exception:
suite_entries = None
if not isinstance(suite_entries, list) or not suite_entries:
return False, "Full roster requires a non-empty suite list"
if isinstance(suite_entries, list) and suite_entries:
seen_suites = set()
for entry in suite_entries:
if not isinstance(entry, dict):
return False, "Full roster row must be a dict"
s_path = entry.get("test", entry.get("suite", entry.get("path")))
if not isinstance(s_path, str) or not s_path:
return False, "Full roster row missing suite path"
if s_path not in required_suites:
return False, f"Unknown full roster suite: {s_path}"
if s_path in seen_suites:
return False, f"Duplicate full roster suite: {s_path}"
if s_path:
seen_suites.add(s_path)
p = repo_root / s_path
if not p.is_file():
return False, f"Full roster suite does not exist on disk: {s_path}"
expected_sha = entry.get("source_sha256")
if not isinstance(expected_sha, str) or not HEX64_PATTERN.fullmatch(expected_sha):
return False, f"Full roster missing or invalid source_sha256: {s_path}"
if expected_sha:
actual_sha = compute_sha256(p)
if actual_sha != expected_sha:
return False, f"Full roster source SHA256 mismatch for {s_path}: expected {expected_sha}, got {actual_sha}"
e_fails = entry.get("failures")
if type(e_fails) is not int or e_fails != 0:
return False, f"Full roster suite {s_path} failed with {e_fails} failures"
if entry.get("passed") is not True:
return False, f"Full roster suite {s_path} passed must be True"
missing_from_roster = set(required_suites) - seen_suites
if seen_suites != set(required_suites):
return False, f"Full roster execution missing required suites: {sorted(list(missing_from_roster))[:5]}"
return True, ""
def verify_evidence(index_path: Path) -> bool:
print(f"Reading evidence index from: {index_path}")
try:
data = json.loads(index_path.read_text(encoding="utf-8"))
except Exception as e:
print(f"ERROR: Failed to read/parse index JSON: {e}", file=sys.stderr)
return False
if not isinstance(data, dict):
print("ERROR: Evidence index root must be a JSON object", file=sys.stderr)
return False
# Requirement A: Task ID, base commit, spec SHA256, and candidate version
task_id = data.get("task_id", data.get("contract_id", data.get("id")))
if task_id != EXPECTED_TASK_ID:
print(f"ERROR: Unexpected task ID: {task_id!r} (expected {EXPECTED_TASK_ID!r})", file=sys.stderr)
return False
base_commit = data.get("base_commit", data.get("commit"))
if base_commit != EXPECTED_BASE_COMMIT:
print(f"ERROR: Base commit mismatch: {base_commit!r} (expected {EXPECTED_BASE_COMMIT!r})", file=sys.stderr)
return False
# spec_sha256 is strictly mandatory
spec_sha = data.get("spec_sha256")
if not spec_sha or not isinstance(spec_sha, str) or spec_sha != EXPECTED_SPEC_SHA256:
print(f"ERROR: Missing or invalid spec_sha256: {spec_sha!r} (expected {EXPECTED_SPEC_SHA256!r})", file=sys.stderr)
return False
# Candidate commit must be strictly valid 40/64 hex and match actual git HEAD
candidate_commit = data.get("candidate_commit", data.get("candidate_head"))
if not candidate_commit or not isinstance(candidate_commit, str):
print(f"ERROR: Missing candidate_commit in evidence index: {candidate_commit!r}", file=sys.stderr)
return False
candidate_commit_norm = candidate_commit.strip().lower()
if not HEX_COMMIT_PATTERN.match(candidate_commit_norm):
print(f"ERROR: candidate_commit must be a valid 40 or 64 character hex SHA, got: {candidate_commit!r}", file=sys.stderr)
return False
if set(candidate_commit_norm) <= {"0"} or set(candidate_commit_norm) <= {"f"}:
print(f"ERROR: candidate_commit is a dummy placeholder hash: {candidate_commit!r}", file=sys.stderr)
return False
actual_head = get_actual_git_head()
if not actual_head:
print("ERROR: Unable to verify real repository git HEAD; git failed or CANDIDATE_COMMIT/TARGET_HEAD mismatched", file=sys.stderr)
return False
if candidate_commit_norm != actual_head:
print(f"ERROR: Candidate commit mismatch: index specifies {candidate_commit_norm!r}, but verified actual candidate git HEAD is {actual_head!r}", file=sys.stderr)
return False
# Scope source manifest verification: all REQUIRED_SCOPE_FILES must be present in source_hashes
sources = data.get("source_hashes")
if not isinstance(sources, dict) or not sources:
print("ERROR: 'source_hashes' dictionary is required in evidence index", file=sys.stderr)
return False
missing_scope_sources = [f for f in REQUIRED_SCOPE_FILES if f not in sources]
if missing_scope_sources:
print(f"ERROR: 'source_hashes' omits required scope files: {missing_scope_sources}", file=sys.stderr)
return False
for rel_path, expected_hash in sources.items():
p = Path(rel_path)
if not p.is_file():
print(f"ERROR: Required source file does not exist on disk: {rel_path}", file=sys.stderr)
return False
if not isinstance(expected_hash, str) or not HEX64_PATTERN.match(expected_hash) or expected_hash == "0" * 64:
print(f"ERROR: Invalid or dummy SHA256 format for {rel_path}: {expected_hash!r}", file=sys.stderr)
return False
actual_hash = compute_sha256(p)
if actual_hash != expected_hash:
print(f"ERROR: Source hash mismatch for {rel_path}: expected {expected_hash}, got {actual_hash}", file=sys.stderr)
return False
# Requirement B: Artifacts are non-optional and strictly validated
artifacts = data.get("artifacts")
if not isinstance(artifacts, dict) or not artifacts:
print("ERROR: 'artifacts' must be a non-empty JSON object mapping artifact IDs to artifact details", file=sys.stderr)
return False
evidence_dir = index_path.parent
validated_artifacts = {}
for art_id, art_info in artifacts.items():
if not isinstance(art_info, dict):
print(f"ERROR: Artifact '{art_id}' must be a dictionary", file=sys.stderr)
return False
rel_art_path = art_info.get("path")
if not rel_art_path or not isinstance(rel_art_path, str):
print(f"ERROR: Artifact '{art_id}' missing valid 'path'", file=sys.stderr)
return False
if ".." in rel_art_path or os.path.isabs(rel_art_path):
print(f"ERROR: Artifact '{art_id}' path traversal or absolute path forbidden: {rel_art_path}", file=sys.stderr)
return False
art_file = evidence_dir / rel_art_path
if not art_file.is_file():
art_file = Path(rel_art_path)
if not art_file.is_file():
print(f"ERROR: Referenced artifact file missing on disk: {rel_art_path}", file=sys.stderr)
return False
if art_file.stat().st_size == 0:
print(f"ERROR: Artifact file is empty: {rel_art_path}", file=sys.stderr)
return False
expected_art_hash = art_info.get("sha256")
if not expected_art_hash or not isinstance(expected_art_hash, str) or not HEX64_PATTERN.match(expected_art_hash) or expected_art_hash == "0" * 64:
print(f"ERROR: Invalid or dummy SHA256 for artifact '{art_id}': {expected_art_hash!r}", file=sys.stderr)
return False
actual_art_hash = compute_sha256(art_file)
if actual_art_hash != expected_art_hash:
print(f"ERROR: SHA256 mismatch for artifact '{art_id}': expected {expected_art_hash}, got {actual_art_hash}", file=sys.stderr)
return False
art_type = art_info.get("type")
if not art_type or not isinstance(art_type, str):
print(f"ERROR: Artifact '{art_id}' missing required 'type'", file=sys.stderr)
return False
validated_artifacts[art_id] = {
"file": art_file,
"type": art_type,
"info": art_info,
}
# Requirement C: OS Proof, PID validation, and binding to actual execution receipt
os_proof = data.get("os_environment")
if not isinstance(os_proof, dict) or not os_proof:
print("ERROR: 'os_environment' is required and cannot be omitted or empty", file=sys.stderr)
return False
pid = os_proof.get("pid")
if isinstance(pid, bool) or not isinstance(pid, int):
print(f"ERROR: 'pid' must be a valid integer, not boolean or other type: {pid!r}", file=sys.stderr)
return False
if pid <= 1 or pid > MAX_VALID_PID or pid == 999999999:
print(f"ERROR: Invalid, dummy, or init/launchd PID: {pid}", file=sys.stderr)
return False
platform_str = os_proof.get("platform")
if not platform_str or not isinstance(platform_str, str):
print("ERROR: Missing or invalid OS platform string in os_environment", file=sys.stderr)
return False
window_size = os_proof.get("window_size")
if not isinstance(window_size, (list, tuple)) or len(window_size) != 2:
print(f"ERROR: Missing or invalid window_size in os_environment: {window_size!r}", file=sys.stderr)
return False
w, h = window_size
if isinstance(w, bool) or isinstance(h, bool) or not isinstance(w, int) or not isinstance(h, int) or w <= 0 or h <= 0:
print(f"ERROR: Window dimensions must be positive integers: {window_size!r}", file=sys.stderr)
return False
receipt_art_id = os_proof.get("receipt_artifact_id")
if not receipt_art_id or receipt_art_id not in validated_artifacts:
print(f"ERROR: os_environment lacks verified 'receipt_artifact_id' linking to an artifact: {receipt_art_id!r}", file=sys.stderr)
return False
receipt_art = validated_artifacts[receipt_art_id]
try:
receipt_data = json.loads(receipt_art["file"].read_text(encoding="utf-8"))
except Exception as e:
print(f"ERROR: Failed to parse execution receipt artifact JSON: {e}", file=sys.stderr)
return False
receipt_pid = receipt_data.get("pid")
if receipt_pid != pid:
print(f"ERROR: OS environment PID {pid} does not match receipt artifact PID {receipt_pid}", file=sys.stderr)
return False
receipt_ws = receipt_data.get("window_size")
if receipt_ws != window_size:
print(f"ERROR: OS environment window_size {window_size!r} does not match receipt artifact window_size {receipt_ws!r}", file=sys.stderr)
return False
# Optional OS input event verification
os_input_ref = data.get("os_input")
if os_input_ref:
if isinstance(os_input_ref, str) and os_input_ref in validated_artifacts:
try:
inp_data = json.loads(validated_artifacts[os_input_ref]["file"].read_text(encoding="utf-8"))
evs = inp_data.get("events")
if not isinstance(evs, list) or not evs:
print("ERROR: OS input artifact missing non-empty 'events' list", file=sys.stderr)
return False
for ev in evs:
if not isinstance(ev, dict) or not ev.get("action") or not ev.get("monotonic_ns"):
print("ERROR: OS input event missing required fields (action, monotonic_ns)", file=sys.stderr)
return False
except Exception as e:
print(f"ERROR: Failed to validate os_input artifact: {e}", file=sys.stderr)
return False
# Requirement D: Conditions and distinct artifact content verification
conditions = data.get("conditions")
if not isinstance(conditions, dict):
print("ERROR: 'conditions' must be a JSON object mapping condition ID to detailed result object", file=sys.stderr)
return False
expected_set = set(REQUIRED_CONDITIONS)
actual_set = set(conditions.keys())
missing = expected_set - actual_set
if missing:
print(f"ERROR: Missing required conditions in evidence index: {sorted(missing)}", file=sys.stderr)
return False
invented = actual_set - expected_set
if invented:
print(f"ERROR: Unknown invented conditions present in evidence index: {sorted(invented)}", file=sys.stderr)
return False
condition_to_artifacts = {}
for cond in REQUIRED_CONDITIONS:
cond_data = conditions[cond]
if not isinstance(cond_data, dict):
print(f"ERROR: Condition '{cond}' must be a structured JSON object with evidence details, not bare boolean or primitive", file=sys.stderr)
return False
passed_val = cond_data.get("passed")
if isinstance(passed_val, bool):
if passed_val is not True:
print(f"ERROR: Condition '{cond}' explicitly marked not passed", file=sys.stderr)
return False
else:
print(f"ERROR: Condition '{cond}' missing strict boolean 'passed': true (value={passed_val!r})", file=sys.stderr)
return False
cond_art_ids = cond_data.get("artifact_ids")
if not isinstance(cond_art_ids, list) or not cond_art_ids:
print(f"ERROR: Condition '{cond}' has no referenced 'artifact_ids'; claims without evidence are strictly rejected", file=sys.stderr)
return False
for aid in cond_art_ids:
if aid not in validated_artifacts:
print(f"ERROR: Condition '{cond}' references unvalidated or missing artifact ID: {aid!r}", file=sys.stderr)
return False
# Validate substantive details
details = cond_data.get("details", cond_data.get("evidence"))
if not details or not isinstance(details, str) or len(details.strip()) < 10:
print(f"ERROR: Condition '{cond}' lacks substantive verification details", file=sys.stderr)
return False
condition_to_artifacts[cond] = cond_art_ids
# Validate content inside the referenced artifacts for this condition
content_verified = False
last_err = "No artifact content checked"
for aid in cond_art_ids:
art = validated_artifacts[aid]
try:
art_content = json.loads(art["file"].read_text(encoding="utf-8"))
except Exception:
art_content = {}
ok, err_msg = _validate_condition_artifact_content(
cond,
art_content,
art["type"],
evidence_dir,
data,
validated_artifacts,
)
if ok:
content_verified = True
break
else:
last_err = err_msg
if not content_verified:
print(f"ERROR: Condition '{cond}' artifact content verification failed: {last_err}", file=sys.stderr)
return False
# Reject self-declaration where conditions share a single generic artifact
unique_referenced_artifacts = set()
for aids in condition_to_artifacts.values():
unique_referenced_artifacts.update(aids)
if len(unique_referenced_artifacts) < len(REQUIRED_CONDITIONS):
print(f"ERROR: All 8 conditions require distinct evidence artifacts; found only {len(unique_referenced_artifacts)} unique artifacts across {len(REQUIRED_CONDITIONS)} conditions", file=sys.stderr)
return False
print("SUCCESS: All 8 required conditions, candidate version, scope manifest, and OS execution artifacts strictly verified.")
return True
def main() -> int:
parser = argparse.ArgumentParser(description="Check P24-02 Operations Evidence")
parser.add_argument("--verify-only", action="store_true", help="Strict verification of existing evidence index only")
parser.add_argument("--index", type=str, help="Path to evidence index.json", default=None)
args = parser.parse_args()
index_file = find_index_file(args.index)
if not index_file:
print("ERROR: Could not locate p24-operations-evidence index.json.", file=sys.stderr)
print("Ensure BUILD_OUTPUT_DIR is set or provide --index <path>.", file=sys.stderr)
return 1
if not verify_evidence(index_file):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())