From f2a1249440d6ce8ccfbbe7e451ab9f7b1332cd35 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 01:51:44 +0900 Subject: [PATCH 1/5] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 473 ++++++++++++++++++ .../p28-01/restart/probe/restart_pair.py | 310 ++++++++++++ .../p28-01/restart/probe/restart_probe.gd | 448 +++++++++++++++++ .../p28-01/restart/probe/test_restart.py | 466 +++++++++++++++++ 4 files changed, 1697 insertions(+) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd create mode 100644 docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py new file mode 100644 index 0000000..04e8371 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +"""RESTART evidence validation; typed state comparisons execute in Godot.""" +import ast +import copy +import hashlib +import importlib.util +import json +from pathlib import Path +import re +import sys +import sysconfig + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[6] +BASE = "docs/plans/original-completion-2026-10-02/execution/p28-01/" +DEPENDENCIES = { + "tools/run_all_tests.py": "320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4", + "tools/regression/owned_outer.py": "c2e3483b76db1a3ff940f23f88ea0083fc78736e874ebcdb385d766dde8866e7", + "tools/regression/owned_diagnostic_process.py": "e31e93c6e2c5ea46a7d954ef8bc0b4caec1c08b86b8aef19e93b52ef24993087", + "tools/regression/p09_owned_engine_process.py": "c007e712c281573254faa1233c2ac92e100ab87393256a6dc096b35283e43037", + BASE + "collector/collect_p28_01.py": "a43e1f963ef8505ddd5acf8bf747d9bfac22a028cbb805c0b964ee11b9b40a14", + BASE + "collector/check_p28_01_report.py": "6c0851d498ca80bdc120fad047b072255f48713f63114521f3d4dfc902b2512b", +} +LOCAL = {BASE + "restart/probe/" + n for n in + ("restart_pair.py", "check_restart.py", "test_restart.py", "restart_probe.gd")} +PHASES = ["import", "parse-base", "parse-wrapper", "R-writer", "R-reader", "O-writer", "O-reader"] +CROSS = [ + {"condition": "原本条件とUI操作が一致", "applicable": False, "reference": "collector C1/C2", + "reason": "既存collectorのUI経路受入を参照。このtaskは再開の比較を担当する。"}, + {"condition": "Runtimeの実利用・会計・行動へ接続", "applicable": False, "reference": "collector C2/C3", + "reason": "既存collectorの実Runtime観測を参照。製品の接続は変更しない。"}, + {"condition": "途中保存・再起動で再現", "applicable": True, "reference": "RS1/RS2/RS3/RS4/RS5", + "reason": "別PIDのR/O writerとreaderで適用する。"}, + {"condition": "異常/境界/中断を検証", "applicable": False, "reference": "collector C5/C6/C7", + "reason": "横断受入は既存collectorを参照。再開固有の異常と境界はRS5で検証する。"}, + {"condition": "関連回帰PASS", "applicable": False, "reference": "collector C8", + "reason": "既存collectorの正式回帰証拠を参照。workerは全回帰を実行しない。"}, + {"condition": "要件別ログとソースハッシュを保存", "applicable": False, "reference": "collector C6/C7/C9", + "reason": "既存collectorの証拠契約を参照。再開のraw結合はRS7で保存する。"}, +] + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def pin(path): + path = Path(path) + require(path.is_file() and not path.is_symlink(), "missing/symlink evidence") + return {"sha256": sha(path), "bytes": path.stat().st_size} + + +def check_dependencies(root=ROOT): + for name, expected in DEPENDENCIES.items(): + require(sha(root / name) == expected, "dependency SHA: " + name) + + +def check_imports(text, external=None): + tree = ast.parse(text) + if external is not None: + require(external in DEPENDENCIES, "external AST closure") + # The explicitly forbidden run_all_tests.main imports are not executed. + tree.body = [node for node in tree.body if not ( + isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == "main")] + allowed = {Path(p).stem for p in set(DEPENDENCIES) | LOCAL} + for node in ast.walk(tree): + if isinstance(node, (ast.Import, ast.ImportFrom)): + names = [node.module] if isinstance(node, ast.ImportFrom) else [a.name for a in node.names] + for name in names: + require(name and (standard_library(name.split(".")[0]) or name in allowed), + "import outside closure") + if isinstance(node, ast.Call): + name = node.func.id if isinstance(node.func, ast.Name) else getattr(node.func, "attr", "") + require(name not in ("__import__", "eval", "import_module"), "dynamic code forbidden") + require(name != "exec" or external == "tools/regression/owned_diagnostic_process.py", + "unapproved exec") + if name == "spec_from_file_location": + for sub in ast.walk(node): + if isinstance(sub, ast.Constant) and isinstance(sub.value, str): + for path in re.findall(r"[\w./-]+\.py", sub.value): + require(path in DEPENDENCIES or path in LOCAL or + path in {Path(p).name for p in DEPENDENCIES}, "loader outside closure") + if external and isinstance(node, ast.BinOp): + for sub in ast.walk(node): + if isinstance(sub, ast.Constant) and isinstance(sub.value, str) and sub.value.endswith(".py"): + require(sub.value in DEPENDENCIES or sub.value in {Path(p).name for p in DEPENDENCIES}, + "external loader path outside closure") + + +def standard_library(name): + # Python 3.9 on the native host has no sys.stdlib_module_names. + spec = importlib.util.find_spec(name) + if spec is None: + return False + if spec.origin in ("built-in", "frozen"): + return True + if not spec.origin: + return False + path = Path(spec.origin).resolve() + return path.is_relative_to(Path(sysconfig.get_path("stdlib")).resolve()) and not any( + part in ("site-packages", "dist-packages") for part in path.parts) + + +def check_modules(modules=None, root=ROOT): + for module in (sys.modules if modules is None else modules).values(): + name = getattr(module, "__file__", None) + if name: + path = Path(name).resolve() + if path.is_relative_to(root): + require(path.relative_to(root).as_posix() in set(DEPENDENCIES) | LOCAL, + "loaded project module outside closure") + + +def check_probe(text): + function = None + blocks = [] + pauses = 0 + for raw in text.splitlines(): + line = raw.split("#", 1)[0] + match = re.match(r"^func (\w+)\(", line) + if match: + function = match[1] + if function == "deterministic_step": + blocks.append([]) + if function == "deterministic_step": + blocks[-1].append(line) + require(not re.search(r"set_paused\s*\(\s*false|\bclear\s*\(|spawn_guest|SimulationRuntime\.new|\binitialize\s*\(|enable_production_|_toggle_pause", line), + "forbidden probe API") + if re.search(r"\badvance_", line): + require(function == "deterministic_step" and line.strip() == "GameClock.advance_minutes(1)", + "advance outside deterministic_step") + if "set_paused(" in line: + require(function == "on_tick" and line.strip() == "GameClock.set_paused(true)", "pause outside tick") + pauses += 1 + require(not re.search(r"Time\.get_ticks_usec|\b\w+_us\b|Performance\.|get_performance_report", line), + "performance instrumentation") + require(len(blocks) == 1 and pauses == 1, "deterministic/tick functions required") + block = "\n".join(blocks[0]) + require(block.count("GameClock.advance_minutes(1)") == 1 and + "io_ready" in block and "GameClock.paused" in block, "step admission") + + +def event_id(counter): + require(type(counter) is int and counter >= 0, "counter type") + return "domain_event.%012d" % (counter + 1) + + +def transform(witness, event): + """Consistency oracle for recorded IO paths, not a Godot state decoder.""" + result = copy.deepcopy(witness) + eid = event_id(witness["counter"]) + ids, recent = witness["published_ids"], witness["recent_events"] + require(event["event_id"] == eid, "IO event ID") + require(ids == sorted(set(ids)) and eid not in ids and len(recent) <= 256, "event history") + require(len({x["event_id"] for x in recent}) == len(recent) and + all(x["event_id"] in ids for x in recent), "recent IDs") + result["counter"] += 1 + result["published_ids"] = sorted(ids + [eid]) + result["recent_events"] = (recent[1:] if len(recent) == 256 else recent) + [event] + return result + + +def exact(row): + require(row["method"] == "var_to_bytes" and row["equal"] is True and row["diff_paths"] == [], + "typed bytes differ") + require(row["excluded_paths"] == [], "state path exclusion") + require(re.fullmatch("[0-9a-f]{64}", row["expected_sha256"]) and + row["actual_sha256"] == row["expected_sha256"], "typed comparison contradiction") + + +def check_io(row, kind): + require(row["kind"] == kind and row["signal_count"] == 1, "one IO signal required") + signal, latest, ev = row["signal_payload"], row["latest"], row["event"] + require(signal == latest == ev["payload"], "signal/latest/event mismatch") + require(row["signal_projection"] == "_save_event_payload" and row["payload_bytes_equal"] is True, + "typed payload comparison") + success = kind != "tamper" + require(type(signal["ok"]) is bool and signal["ok"] is success and + ((signal["code"] == "OK") is success), "IO result") + require(signal["operation"] == ("save" if kind == "save" else "load"), "operation") + require(signal["save_path"].startswith("user://p28_restart/") and ".." not in signal["save_path"], + "private IO path") + require(row["return_kind"] == ("bool" if kind == "save" else "void"), "return kind") + require(row["return_value"] is signal["ok"] if kind == "save" else row["return_value"] is None, + "return/signal mismatch") + expected_type = {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", + "tamper": "SAVE_LOAD_FAILED"}[kind] + require(ev["event_type"] == expected_type and ev["source_service"] == "save_service" and + ev["entity_ids"] == [] and ev["causation_command_id"] == 0, "event shape") + require(ev["absolute_minute"] == row["minute_after"] == row["minute_before"], "IO minute") + require(row["after"] == transform(row["before"], ev), "not X(P,event)") + require(row["dropped"] == (row["before"]["recent_events"][0] + if len(row["before"]["recent_events"]) == 256 else None), "wrong dropped head") + exact(row["comparison"]) + if success: + require(signal["saved_absolute_minute"] == row["minute_after"] and bool(signal["save_id"]), + "save identity") + require(row["paused_after"] is True, "IO clock not paused") + else: + failure = row["failure"] + require(failure["preload_pause_restored"] is True and + failure["loaded_clock_paused"] is row["paused_before"] and + row["paused_after"] is row["paused_before"], "tamper pause") + if failure["recovery_report"] is not None: + require(failure["recovery_report"]["restore_committed"] is False, "tamper restore") + require(type(failure["migration_attempted"]) is bool, "migration observation") + + +def check_report(report): + check_no_metrics(report) + require(report["schema"] == "p28-restart-v1" and report["stop_reason"] == "complete", "incomplete probe") + require(report["role"] in PHASES[3:] and type(report["pid"]) is int and report["pid"] > 0, "role/PID") + require(report["main_added"] is True and report["k_target"] == 30 and report["actual_k"] == 30, "main/K") + require(report["comparison_scope"] == "complete_typed_var_to_bytes" and report["excluded_paths"] == [], + "partial digest/path exclusion") + require(report["transfer_before_guard_writes"] == 0 and report["probe_deadline_seconds"] == 1740, + "profile/deadline") + writer = report["role"].endswith("writer") + kinds = ["save"] if writer else ["tamper", "load"] + require(len(report["io"]) == len(kinds), "IO sequence") + for row, kind in zip(report["io"], kinds): + check_io(row, kind) + if writer: + exact(report["package_matches_before"]) + require(report["package_matches_before"]["actual_sha256"] == report["states"]["P"]["sha256"], + "P not package snapshot") + else: + require(report["failure_event_absent"] is True and report["transfer_verified"] is True, + "failed event survived/transfer not verified") + io = report["io"][-1] + require(report["event_id"] == io["event"]["event_id"] and report["saved_minute"] == io["minute_after"], + "continuation IO identity") + require(report["paused_idle"]["before"] == report["paused_idle"]["after"], "paused clock consumed backlog") + require(report["paused_idle"]["before"]["paused"] is True, "paused observation") + exact(report["paused_idle"]["comparison"]) + rows = report["minutes"] + require(len(rows) == 30, "actual K not 30") + previous, lost = report["saved_minute"], False + for row in rows: + require(row["before"] == previous and row["after"] == previous + 1, "K counts actual minute advances") + require(row["paused"] is True and row["backlog_before"] == row["backlog_after"], "step consumed backlog") + require(row["scope"] == "complete_typed_var_to_bytes" and row["excluded_paths"] == [], "partial state") + require(re.fullmatch("[0-9a-f]{64}", row["sha256"]), "minute SHA") + if row["replacement"] is not None: + require(not lost and row["replacement"] == io["event"] and row["replacement_count"] == 1, + "replacement must be unchanged in-window E") + else: + lost = True + require(row["replacement_count"] == 0, "replacement after E left window") + require(0 <= row["recent_count"] <= 256, "recent limit") + require(len(row["recent_ids"]) == len(set(row["recent_ids"])) == row["recent_count"] and + row["recent_ids"].count(report["event_id"]) == row["replacement_count"], + "replacement does not match actual window") + previous = row["after"] + require(report["final_minute"] == previous, "final minute") + for name, entry in report["states"].items(): + require(re.fullmatch("[0-9a-f]{64}", entry["sha256"]) and entry["bytes"] > 0 and + entry["path"] == "p28_restart/evidence/" + name + ".bin", "state binding") + required = {"P", "after_save", "final"} if writer else {"P", "B", "after_tamper", "after_load", "final"} + require(set(report["states"]) == required, "missing complete state files") + require(report["states"]["after_save" if writer else "after_load"]["sha256"] == + io["comparison"]["actual_sha256"], "IO state file differs") + if not writer: + require(report["states"]["after_tamper"]["sha256"] == report["io"][0]["comparison"]["actual_sha256"], + "tamper state file differs") + if report["role"].startswith("O-"): + boundary = report["boundary"] + require(boundary is not None and boundary["guest_auto_count"] > 0, "arrival not processed") + require(len(boundary["guest_auto_ids"]) == len(set(boundary["guest_auto_ids"])) == + boundary["guest_auto_count"], "arrival IDs") + require(all(x.startswith("guest-auto-") for x in boundary["guest_auto_ids"]), "arrival identity") + require(boundary["clock"] == report["saved_minute"] and boundary["processed_arrival_minutes"], + "arrival boundary not processed") + if writer: + require(0 < report["saved_minute"] - report["start_minute"] <= 120, "arrival cap") + ticks = report["ui_ticks"] + require(ticks and ticks[-1] == boundary and all(t["guest_auto_count"] == 0 for t in ticks[:-1]), + "not first arrival tick") + next_boundary = report["next_boundary"] + added = [m for m in next_boundary["after"] if m not in next_boundary["before"]] + require(next_boundary["before"] == boundary["processed_arrival_minutes"] and + added == [next_boundary["minute"]] and next_boundary["processed_once"] is True and + next_boundary["minute"] > boundary["clock"] and + next_boundary["minute"] <= report["final_minute"], "next arrival missing/duplicate") + minutes = next_boundary["after"] + require(minutes == sorted(set(minutes)), "duplicate processed arrivals") + require([r["minute"] for r in next_boundary["arrival_reports"]] == minutes, + "arrival report duplicates/omissions") + else: + require(report["boundary"] is None and report["next_boundary"] is None, "R Operations boundary") + return True + + +def check_no_metrics(value): + if isinstance(value, dict): + for key, child in value.items(): + # Mandatory verbatim domain/IO payloads contain product diagnostics. + # They are evidence, never performance measurements or verdicts. + if key in ("payload", "signal_payload", "latest"): + continue + require(not key.endswith(("_us", "_ms")) and key not in + ("performance", "statistics", "fps", "budget_verdict"), "performance field forbidden") + check_no_metrics(child) + elif isinstance(value, list): + for child in value: + check_no_metrics(child) + + +def check_pair(writer, reader): + check_report(writer) + check_report(reader) + require(writer["role"].replace("writer", "reader") == reader["role"] and writer["pid"] != reader["pid"], + "pair roles/PID") + require(writer["event_id"] == reader["event_id"] and writer["saved_minute"] == reader["saved_minute"], + "pair IO identity") + require(writer["states"]["P"] == reader["states"]["P"], "package P differs") + require(writer["io"][0]["before"] == reader["io"][1]["before"], "package history differs") + for left, right in zip(writer["minutes"], reader["minutes"]): + require(all(left[k] == right[k] for k in + ("before", "after", "sha256", "scope", "replacement_count", "recent_count")), + "cross-PID complete typed state mismatch") + require(writer["boundary"] == reader["boundary"] and writer["next_boundary"] == reader["next_boundary"], + "Operations boundary differs") + + +def bound_raw(folder, row): + folder = Path(folder) + require(set(row["raw"]) == {"stdout.log", "stderr.log", "engine-native.log"}, "all raw required") + for name, expected in {**row["raw"], **row["owned_files"]}.items(): + require(Path(name).name == name and pin(folder / name) == expected, "raw/owned SHA mismatch") + require(set(row["owned_files"]) == {"process.json", "started.json", "ended.json", + "adapter-owned-cleanup.json", "outer-owned-finally.json"}, "owned receipts missing") + require(json.loads((folder / "process.json").read_text()) == row["process"], "process receipt mismatch") + process, capture = row["process"], row["capture"] + require(row["passed"] is True and process["exit_code"] == 0 and process["helper_exit_code"] == 0 and + process["timed_out"] is False and process["error_lines"] == [] and process["lifecycle_error"] is None and + process["external_process_or_group_signals"] == 0 and process["owned_popen_ended"] is True and + process["old_helper_engine_ended"] is True and process["start_receipt_written"] is True and + process["end_receipt_written"] is True, "owned phase failed") + require(capture["passed"] is True and capture["all_captured_ended"] is True and + capture["outer_cleanup_errors"] == [] and capture["adapter_exception"] is None and + capture["foreign_process_or_group_signals"] == 0 and + capture["all_cleanup_before_outer_receipt"] is True and capture["outer_receipt_written"] is True and + capture["outer_receipt_error"] is None and len(capture["actual_captured_pids"]) == 1, "outer capture") + actual = json.loads((folder / "outer-owned-finally.json").read_text()) + require(actual == {k: v for k, v in capture.items() if k not in + ("passed", "outer_receipt_written", "outer_receipt_error")}, "outer raw differs") + pid = capture["actual_captured_pids"][0] + require(process["pid"] == pid and process["argv"] == row["argv"], "process PID/argv") + for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")): + binding = json.loads((folder / filename).read_text()) + require(binding == process[key] and binding["pid"] == binding["actual_pid"] == pid and + binding["argv"] == binding["command"] == row["argv"] and binding["owner_pid"] > 0, + "START/END binding") + require(process["actual_start_binding"]["owner_pid"] == process["actual_end_binding"]["owner_pid"], + "START/END owners differ") + require(json.loads((folder / "adapter-owned-cleanup.json").read_text()) == process["adapter_owned_cleanup"] + and process["adapter_owned_cleanup"]["engine_ended"] is True and + process["adapter_owned_cleanup"]["errors"] == [], "adapter cleanup binding") + require(process["stdout_sha256"] == row["raw"]["stdout.log"]["sha256"] and + process["stderr_sha256"] == row["raw"]["stderr.log"]["sha256"], "process raw binding") + require(row["timeout_seconds"] == 1800 and row["diagnostics"] == [], "phase timeout/diagnostics") + raw = (folder / "stdout.log").read_text() + for name in ("stdout.log", "stderr.log", "engine-native.log"): + require(not re.search(r"SCRIPT ERROR|Parse Error|ERROR:", (folder / name).read_text()), + "raw engine error") + if row["phase"] not in PHASES[3:]: + require("P28_RESTART_REPORT=" not in raw, "parse/import report") + if row["phase"].startswith("parse"): + require("--check-only" in row["argv"], "parse not check-only") + return None + reports = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_REPORT=")] + closes = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_RESTART_AUDIO_CLOSE=")] + require(len(reports) == len(closes) == 1, "report/audio close count") + report, close = json.loads(reports[0]), json.loads(closes[0]) + require(report == row["report"] and report["role"] == row["phase"] and + capture["actual_captured_pids"] == [report["pid"]], "raw report/PID binding") + require(close.get("coordinator") is True and close.get("passed") is True and + close.get("owned_engine_pid") == report["pid"] and close.get("all_owned_targets_released") is True and + close.get("deadline_reached") is False and close.get("clock_process_held_stopped") is True and + close.get("window_input_held_disabled") is True and close.get("restored_before_quit") is False and + close.get("shared_bus_deleted") is False and close.get("settings_mutated") is False, "audio close") + check_report(report) + return report + + +def check_receipt(receipt, out): + from restart_pair import COLLECTOR_PINS, check_transfer + out = Path(out) + require(receipt["passed"] is True and receipt["error"] is None and receipt["cleanup_errors"] == [], + "execution failed") + require(receipt["source_before"] == receipt["source_after"] and len(receipt["source_before"]) == 1407, + "source map mismatch") + require(receipt["default_player_before"] == receipt["default_player_after"], "ordinary player changed") + require(receipt["dependencies_before"] == receipt["dependencies_after"] == DEPENDENCIES, "dependency pins") + require(receipt["collector_before"] == receipt["collector_after"] == COLLECTOR_PINS, + "collector changed") + require(receipt["worker_before"] == receipt["worker_after"] and set(receipt["worker_before"]) == + {"restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py"}, "worker changed") + require(receipt["cross_cutting"] == CROSS, "cross-cutting record") + require(receipt["original_credit"] == 0 and receipt["windows_verified"] is False and + receipt["operations_200_guests_verified"] is False, "scope expanded") + rows = receipt["phases"] + require([r["phase"] for r in rows] == PHASES, "phase order/incomplete execution") + pids = [] + for row in rows: + report = bound_raw(out / row["phase"], row) + pids.extend(row["capture"]["actual_captured_pids"]) + require(row["stage_before"] == row["stage_after"] if row["phase"] != "import" + else row["stage_preserved"] is True, "stage changed") + require(row["source_preserved"] is True and row["player_preserved"] is True, "phase preservation") + startup = row["startup_project"] + require(startup["fresh_before"] is True and sha(startup["path"]) == startup["sha256"], "startup profile") + if report: + guard = row["guard"] + expected = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5, + "actual_user_data_dir": row["runtime_profile"], + "expected_user_data_dir": row["runtime_profile"], + "globalized_probe": str(Path(row["runtime_profile"]) / "guard-probe"), + "custom_user_dir_name": row["runtime_name"], "fresh_before_parent_initialize": True, + "profile_writes_before_guard": 0, "project_settings_persisted": False} + require(all(type(guard.get(k)) is type(v) and guard[k] == v for k, v in expected.items()), + "guard mismatch") + guard_path = out / (row["phase"] + "-guard.json") + require(sha(guard_path) == row["guard_sha256"] and json.loads(guard_path.read_text()) == guard, + "guard raw binding") + for name, evidence in report["states"].items(): + require(pin(out / (row["phase"] + "-states") / (name + ".bin")) == + {k: evidence[k] for k in ("sha256", "bytes")}, "state bytes SHA") + require(len(set(pids)) == len(pids), "reused PID") + require({r["pid"] for r in receipt["cleanup"]} == set(pids) and all( + r["engine_ended"] is True and r["outer_cleanup_errors"] == [] and + r["foreign_process_or_group_signals"] == 0 for r in receipt["cleanup"]), "cleanup mismatch") + for start in (3, 5): + check_pair(rows[start]["report"], rows[start + 1]["report"]) + transfer = out / (rows[start]["phase"][0] + "-transfer") + manifest_path = transfer / "manifest.json" + manifest = json.loads(manifest_path.read_text()) + check_transfer(transfer, manifest) + require(rows[start + 1]["report"]["transfer_manifest_sha256"] == sha(manifest_path), + "reader manifest SHA") + for phase_index, kinds in ((start, ("good",)), (start + 1, ("good", "tampered"))): + profile = rows[phase_index]["runtime_profile"] + for kind in kinds: + for name, expected in manifest["files"][kind].items(): + relative = "p28_restart/" + kind + "/" + name + require(receipt["profiles"][profile].get(relative) == expected, + "private package differs from transfer") + for path, expected in receipt["script_pins"].items(): + require(sha(path) == expected, "executed script changed") + check_probe((out / "restart_probe.gd").read_text()) + require(sha(out / "restart_probe.gd") == receipt["worker_before"]["restart_probe.gd"], + "executed probe differs from submitted source") + require(receipt["owned_writes"] == [], "probe/driver wrote owned folder") + return True + + +def main(): + import argparse + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("execution", type=Path) + args = parser.parse_args() + check_receipt(json.loads(args.execution.read_text()), args.execution.parent) + print("P28_RESTART_CHECK=PASS") + + +if __name__ == "__main__": + main() diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py new file mode 100644 index 0000000..b565c03 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Root-only four-PID RESTART driver. --check performs no writes or launches.""" +import argparse +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import uuid + +sys.dont_write_bytecode = True +import check_restart as check + +HERE, ROOT = check.HERE, check.ROOT +COLLECTOR = check.BASE + "collector/collect_p28_01.py" +COLLECTOR_PINS = { + "collect_p28_01.py": check.DEPENDENCIES[COLLECTOR], + "check_p28_01_report.py": check.DEPENDENCIES[check.BASE + "collector/check_p28_01_report.py"], + "measure_p28_01.gd": "4acef88bce4ac77f3f5d2659d766afcc5fd139cee381118839012022772946bb", + "test_collect_p28_01.py": "99d86ea723c976c95dbd283acbb85981eabf427b30994e652002ec0e76dc4ab0", +} + + +def load_dependencies(): + check.check_dependencies() + for name in check.DEPENDENCIES: + check.check_imports((ROOT / name).read_text(), external=name) + for name in ("restart_pair.py", "check_restart.py", "test_restart.py"): + check.check_imports((HERE / name).read_text()) + check.check_modules() + sys.path.insert(0, str((ROOT / COLLECTOR).parent)) + spec = importlib.util.spec_from_file_location("restart_collector", ROOT / COLLECTOR) + collector = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = collector + spec.loader.exec_module(collector) + standard = collector.load_tool(collector.EXTERNAL_IMPORTS[0]) + outer = collector.load_tool(collector.EXTERNAL_IMPORTS[1]) + check.check_modules() + check.check_modules({"collector": collector, "standard": standard, "outer": outer, "native": outer.native}) + check.require(Path(outer.native.helper["__file__"]).resolve() == + ROOT / "tools/regression/p09_owned_engine_process.py", "exec helper closure") + return collector, standard, outer + + +def collector_pins(): + actual = {name: check.sha((ROOT / COLLECTOR).parent / name) for name in COLLECTOR_PINS} + check.require(actual == COLLECTOR_PINS, "collector differs from Q") + return actual + + +def plan(collector, engine, out): + stage, wrapper = out / "stage", out / "guarded.gd" + return [collector.argv_for(engine, stage, wrapper, True), + collector.parse_argv(engine, stage, out / "restart_probe.gd"), + collector.parse_argv(engine, stage, wrapper)] + [ + collector.argv_for(engine, stage, wrapper) for _ in range(4)] + + +def schedule(rows): + for index, label in enumerate(check.PHASES): + check.require(all(row["passed"] is True for row in rows), "previous phase failed") + if label.endswith("reader"): + check.require(rows and rows[-1]["phase"] == label.replace("reader", "writer") and + rows[-1].get("report", {}).get("stop_reason") == "complete", + "reader requires successful writer") + yield index, label + check.require(len(rows) == index + 1 and rows[-1]["phase"] == label and rows[-1]["passed"] is True, + "phase did not complete") + + +def safe_inventory(directory): + directory = Path(directory) + check.require(directory.is_dir() and not directory.is_symlink(), "package missing/symlink") + result = {} + for path in sorted(directory.rglob("*")): + check.require(not path.is_symlink(), "package symlink") + if path.is_file(): + result[path.relative_to(directory).as_posix()] = check.pin(path) + check.require(result, "empty package") + return result + + +def create_transfer(source, destination, write): + """Copy only after writer exit; never pre-populate the reader profile.""" + before = safe_inventory(source) + check.require("world.bin" in before and not destination.exists(), "transfer admission") + destination.mkdir() + shutil.copytree(source, destination / "good") + check.require(safe_inventory(destination / "good") == before, "good copy differs") + shutil.copytree(destination / "good", destination / "tampered") + world = destination / "tampered/world.bin" + data = bytearray(world.read_bytes()) + check.require(data, "empty world") + offset = len(data) // 2 + data[offset] ^= 1 + world.write_bytes(data) + manifest = {"files": {"good": before, "tampered": safe_inventory(destination / "tampered")}, + "tamper": {"file": "world.bin", "offset": offset, "xor": 1, + "before_sha256": before["world.bin"]["sha256"], "after_sha256": check.sha(world)}} + write(destination / "manifest.json", manifest) + check_transfer(destination, manifest) + check.require(safe_inventory(source) == before, "writer package changed") + return manifest + + +def check_transfer(directory, manifest): + check.require(set(manifest["files"]) == {"good", "tampered"}, "transfer kinds") + for kind in ("good", "tampered"): + check.require(safe_inventory(directory / kind) == manifest["files"][kind], "transfer SHA mismatch") + good = (directory / "good/world.bin").read_bytes() + bad = (directory / "tampered/world.bin").read_bytes() + tamper = manifest["tamper"] + check.require(len(good) == len(bad) > 0 and tamper["offset"] == len(good) // 2 and tamper["xor"] == 1, + "tamper location") + expected = bytearray(good) + expected[len(good) // 2] ^= 1 + check.require(bytes(expected) == bad and tamper["before_sha256"] == check.sha(directory / "good/world.bin") + and tamper["after_sha256"] == check.sha(directory / "tampered/world.bin"), "tamper bytes") + for name, value in manifest["files"]["good"].items(): + if name != "world.bin": + check.require(manifest["files"]["tampered"].get(name) == value, "extra tamper difference") + check.require(set(manifest["files"]["good"]) == set(manifest["files"]["tampered"]), "tamper inventory") + + +def fresh_profile(path): + check.require(not path.exists(), "profile must be fresh; pre-guard writes forbidden") + + +def committed_source(source, commit): + """Read-only Git operations: the 1407 files must be the actual HEAD bytes.""" + tracked = subprocess.check_output(["git", "-C", str(ROOT), "ls-tree", "-r", commit], text=True) + selected = {} + for line in tracked.splitlines(): + metadata, name = line.split("\t", 1) + if name.split("/", 1)[0] in ("autoload", "scripts", "scenes", "data", "tests", "assets", "tools") or \ + name in ("project.godot", "export_presets.cfg", "Makefile", "AGENTS.md"): + if Path(name).name != ".DS_Store" and "__pycache__" not in Path(name).parts: + selected[name] = metadata.split()[2] + check.require(set(selected) == set(source), "source map differs from HEAD") + for name, expected in selected.items(): + actual = subprocess.check_output(["git", "-C", str(ROOT), "hash-object", str(ROOT / name)], text=True).strip() + check.require(actual == expected, "dirty product source: " + name) + + +def bind_report(out, label, row): + text = (out / label / "stdout.log").read_text() + reports = [s.partition("=")[2] for s in text.splitlines() if s.startswith("P28_RESTART_REPORT=")] + check.require(len(reports) == 1, "one raw report required") + row["report"] = json.loads(reports[0]) + check.bound_raw(out / label, row) + return row["report"] + + +def copy_states(out, row): + folder = out / (row["phase"] + "-states") + folder.mkdir() + profile = Path(row["runtime_profile"]) + for name, expected in row["report"]["states"].items(): + source = profile / expected["path"] + check.require(source.resolve().is_relative_to(profile.resolve()) and + check.pin(source) == {k: expected[k] for k in ("bytes", "sha256")}, "state binding") + shutil.copyfile(source, folder / (name + ".bin")) + check.require(check.pin(source) == check.pin(folder / (name + ".bin")), "state copy differs") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + mode = parser.add_mutually_exclusive_group(required=True) + mode.add_argument("--check", action="store_true") + mode.add_argument("--run", action="store_true") + for name in ("readonly", "fixture", "engine", "output"): + parser.add_argument("--" + name, type=Path, required=True) + args = parser.parse_args() + collector, standard, outer = load_dependencies() + pinned = collector_pins() + readonly, fixture, engine, out = (p.resolve() for p in (args.readonly, args.fixture, args.engine, args.output)) + inputs = collector.inputs(readonly) + check.require(check.sha(fixture) == collector.FIXTURE_SHA and check.sha(engine) == collector.ENGINE_SHA, + "fixture/engine SHA") + check.check_probe((HERE / "restart_probe.gd").read_text()) + source = collector.source_map(standard, ROOT) + check.require(len(source) == 1407, "source count") + commit = subprocess.check_output(["git", "-C", str(ROOT), "rev-parse", "HEAD"], text=True).strip() + committed_source(source, commit) + worker_pins = {name: check.sha(HERE / name) for name in + ("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")} + commands = plan(collector, engine, out) + admission = {"commit": commit, "source_map": source, "readonly": inputs, "planned_argv": commands, + "engine_sha256": check.sha(engine), "fixture_sha256": check.sha(fixture), + "dependencies": check.DEPENDENCIES, "collector_Q": pinned, + "worker_pins": worker_pins, + "phases": check.PHASES, "engines_started": 0} + check.require(not out.exists(), "new output required") + if args.check: + print(json.dumps(admission, ensure_ascii=False, indent=2)) + return 0 + check.require(sys.platform == "darwin", "reviewed native platform is macOS") + collector.no_live_engine() + check.require(shutil.disk_usage(out.parent).free >= 1200000000, "disk space") + player = standard.default_player(ROOT) + out.mkdir(parents=True) + collector.write(out / "admission.json", admission) + rows, profiles = [], [] + error = None + scripts = {} + receipt = {} + try: + stage = out / "stage" + stage.mkdir() + for name in source: + target = stage / name + target.parent.mkdir(parents=True, exist_ok=True) + standard.copy_source(ROOT / name, target) + expected = standard.inventory(stage, stage=True) + check.require(expected == source, "stage source differs") + probe, guard, wrapper = (out / n for n in ("restart_probe.gd", "profile_guard.gd", "guarded.gd")) + probe.write_bytes((HERE / probe.name).read_bytes()) + guard.write_bytes((readonly / "run-r02/profile_guard.gd").read_bytes()) + wrapper.write_text("extends " + json.dumps(str(probe)) + "\nconst Guard = preload(" + + json.dumps(str(guard)) + ")\nfunc _initialize() -> void:\n" + "\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n") + scripts = {str(p): check.sha(p) for p in (probe, guard, wrapper)} + public = (stage / "project.godot").read_bytes() + for index, label in schedule(rows): + check.check_dependencies() + collector_pins() + check.check_modules() + check.require(collector.source_map(standard, ROOT) == source and standard.default_player(ROOT) == player, + "source/player changed before phase") + check.require(standard.inventory(stage, stage=True) == expected and + all(check.sha(p) == value for p, value in scripts.items()) and + all(check.sha(HERE / p) == value for p, value in worker_pins.items()), + "stage/scripts changed") + startup_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-startup" + runtime_name = "TheTree-PCK-tests-" + uuid.uuid4().hex + "-restart-runtime" + startup, runtime = (standard.profile_path(n) for n in (startup_name, runtime_name)) + fresh_profile(startup) + fresh_profile(runtime) + profiles.extend([startup, runtime]) + transfer = out / (label[0] + "-transfer") + env = {"THE_TREE_PACK_PROFILE_NAME": runtime_name, "THE_TREE_PACK_PROFILE_EXPECTED": str(runtime), + "THE_TREE_PACK_PROFILE_REPORT": str(out / (label + "-guard.json")), + "THE_TREE_RESTART_ROLE": label, "THE_TREE_RESTART_K": "30", + "THE_TREE_RESTART_TRANSFER": str(transfer), + "THE_TREE_RESTART_TRANSFER_MANIFEST": str(transfer / "manifest.json"), + "THE_TREE_P28_FIXTURE": str(fixture), "THE_TREE_P28_FIXTURE_SHA": collector.FIXTURE_SHA} + if label.endswith("reader"): + check_transfer(transfer, json.loads((transfer / "manifest.json").read_text())) + before_stage = collector.digest(expected) + row = collector.run_private_engine(standard, outer, commands[index], out / label, env, + stage, public, startup_name) + row["phase"] = label + rows.append(row) + check.require(row["passed"], "owned engine failure: " + label) + after_stage = standard.inventory(stage, stage=True) + collector.stage_gate(expected, after_stage, importing=label == "import") + row.update(stage_before=before_stage, stage_after=collector.digest(after_stage), stage_preserved=True, + source_preserved=collector.source_map(standard, ROOT) == source, + player_preserved=standard.default_player(ROOT) == player) + expected = after_stage + if label in check.PHASES[3:]: + guard_path = out / (label + "-guard.json") + observation = json.loads(guard_path.read_text()) + collector.guard_gate(observation, runtime, runtime_name) + row.update(runtime_profile=str(runtime), runtime_name=runtime_name, + guard=observation, guard_sha256=check.sha(guard_path)) + report = bind_report(out, label, row) + copy_states(out, row) + collector.write(out / (label + "-report.json"), report) + if label.endswith("writer"): + create_transfer(runtime / "p28_restart/good", transfer, collector.write) + else: + check.require(report["transfer_manifest_sha256"] == check.sha(transfer / "manifest.json"), + "reader transfer manifest binding") + check.check_pair(rows[-2]["report"], report) + else: + check.bound_raw(out / label, row) + check.require(row["source_preserved"] and row["player_preserved"] and + check.sha(fixture) == collector.FIXTURE_SHA and + all(check.sha(p) == value for p, value in scripts.items()), "post-phase preservation") + except BaseException as caught: + error = repr(caught) + finally: + cleanup, cleanup_errors = outer.finish_captured(outer.OWNED) + receipt = {"admission": admission, "phases": rows, "error": error, "passed": error is None, + "source_before": source, "source_after": collector.source_map(standard, ROOT), + "default_player_before": player, "default_player_after": standard.default_player(ROOT), + "dependencies_before": check.DEPENDENCIES, + "dependencies_after": {p: check.sha(ROOT / p) for p in check.DEPENDENCIES}, + "collector_before": pinned, + "collector_after": {p: check.sha((ROOT / COLLECTOR).parent / p) for p in pinned}, + "script_pins": scripts, "cleanup": cleanup, "cleanup_errors": cleanup_errors, + "worker_before": worker_pins, + "worker_after": {p: check.sha(HERE / p) for p in worker_pins}, + "profiles": {str(p): collector.profile_files(p) for p in profiles}, "owned_writes": [], + "cross_cutting": check.CROSS, "original_credit": 0, + "windows_verified": False, "operations_200_guests_verified": False} + if receipt["passed"]: + try: + check.check_receipt(receipt, out) + except (ValueError, KeyError, TypeError, OSError) as caught: + receipt.update(passed=False, error="checker: " + repr(caught)) + collector.write(out / "execution.json", receipt) + return 0 if receipt["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd new file mode 100644 index 0000000..1d62300 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -0,0 +1,448 @@ +extends SceneTree +## Root-owned execution only. All files are written after Guard.activate(). +var GameClock: Variant +var runtime: Variant +var main: Variant +var report: Dictionary = {} +var signals: Array = [] +var key_forms: Dictionary = {} +var role := "" +var start_minute := 0 +var selected_tick := false +var overloaded := false +var io_ready := false +var finished := false +var E := "" +var package: Dictionary = {} +var failure_event: Dictionary = {} +var boundary: Variant = null + +func field(d: Variant, name: String) -> Variant: + if d is Dictionary: + if d.has(StringName(name)): + key_forms[name] = "StringName" + return d[StringName(name)] + if d.has(String(name)): + key_forms[name] = "String" + return d[String(name)] + key_forms[name] = "missing_product_field" + return null + +func _initialize() -> void: + call_deferred("begin") + +func bytes_hash(data: PackedByteArray) -> String: + var context := HashingContext.new() + context.start(HashingContext.HASH_SHA256) + context.update(data) + return context.finish().hex_encode() + +func runtime_part(snapshot: Dictionary) -> Dictionary: + return field(field(snapshot, "systems"), "runtime") + +func witness(snapshot: Dictionary) -> Dictionary: + var part := runtime_part(snapshot) + var events: Dictionary = field(part, "domain_events") + return {"counter": field(field(field(part, "id_generator"), "counters"), "domain_event"), + "published_ids": field(events, "published_ids").duplicate(true), + "recent_events": field(events, "recent_events").duplicate(true)} + +func different_paths(a: Variant, b: Variant, path := "$", depth := 0) -> Array: + var result: Array = [] + if var_to_bytes(a) == var_to_bytes(b): + return result + if depth >= 4 or typeof(a) != typeof(b): + return [path] + if a is Dictionary: + for key: Variant in a: + if not b.has(key): + result.append(path + "." + str(key)) + else: + result.append_array(different_paths(a[key], b[key], path + "." + str(key), depth + 1)) + for key: Variant in b: + if not a.has(key): + result.append(path + "." + str(key)) + elif a is Array and a.size() == b.size(): + for index in range(a.size()): + result.append_array(different_paths(a[index], b[index], path + "[%d]" % index, depth + 1)) + else: + result.append(path) + # Typed container/key/order differences can exist without value differences. + if result.is_empty(): + result.append(path) + return result + +func compare(expected: Dictionary, actual: Dictionary) -> Dictionary: + var a := var_to_bytes(expected) + var b := var_to_bytes(actual) + return {"method": "var_to_bytes", "equal": a == b, "expected_sha256": bytes_hash(a), + "actual_sha256": bytes_hash(b), "excluded_paths": [], + "diff_paths": different_paths(expected, actual)} + +func record_state(name: String, value: Dictionary) -> bool: + var path := "user://p28_restart/evidence/" + name + ".bin" + if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(path).get_base_dir()) != OK: + return false + var data := var_to_bytes(value) + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + return false + file.store_buffer(data) + file.close() + report.states[name] = {"path": path.trim_prefix("user://"), "bytes": data.size(), + "sha256": bytes_hash(data)} + return FileAccess.get_sha256(path) == report.states[name].sha256 + +func on_save(result: Dictionary) -> void: + signals.append(result.duplicate(true)) + +func auto_ids() -> Array: + var ids: Array = [] + for guest: Variant in runtime.guest_service.get_guests(): + if str(guest.guest_id).begins_with("guest-auto-"): + ids.append(str(guest.guest_id)) + ids.sort() + return ids + +func operations_observation() -> Dictionary: + var ids := auto_ids() + var processed: Array = [] + var arrivals: Array = [] + if runtime.operations_coordinator != null: + var state: Dictionary = runtime.operations_coordinator.create_snapshot() + processed = field(state, "processed_arrival_minutes").duplicate() + arrivals = runtime.operations_coordinator.get_arrival_reports().duplicate(true) + return {"clock": GameClock.absolute_minute, "guest_auto_ids": ids, "guest_auto_count": ids.size(), + "operations": runtime.get_operations_report(), "processed_arrival_minutes": processed, + "arrival_reports": arrivals} + +func on_tick(minute: int) -> void: + if io_ready or finished or selected_tick: + return + var observed := operations_observation() + if role == "O-writer": + report.ui_ticks.append(observed) + if minute == 390: + report["at_390"] = observed.duplicate(true) + report.at_390["domain_events"] = field(runtime_part(runtime.create_snapshot()), "domain_events") + var stop: bool = (role == "R-writer" and minute - start_minute >= 10) or \ + (role == "O-writer" and (observed.guest_auto_count > 0 or minute - start_minute >= 120)) + if stop: + GameClock.set_paused(true) + selected_tick = true + +func on_overload(backlog: int) -> void: + overloaded = true + report.overloads.append({"minute": GameClock.absolute_minute, "backlog": backlog}) + +func clock_observation() -> Dictionary: + return {"minute": GameClock.absolute_minute, "paused": GameClock.paused, + "backlog": GameClock.get_backlog_minutes(), + "accumulator_hex": PackedFloat64Array([GameClock.get("_accumulator")]).to_byte_array().hex_encode()} + +func begin() -> void: + GameClock = root.get_node("GameClock") + runtime = root.get_node("SimulationRuntime") + role = OS.get_environment("THE_TREE_RESTART_ROLE") + report = {"schema": "p28-restart-v1", "role": role, "pid": OS.get_process_id(), + "main_added": false, "k_target": 30, "actual_k": 0, "io": [], "states": {}, + "minutes": [], "ui_ticks": [], "overloads": [], "boundary": null, "next_boundary": null, + "comparison_scope": "complete_typed_var_to_bytes", "excluded_paths": [], + "transfer_before_guard_writes": 0, "transfer_verified": false, + "probe_deadline_seconds": 1740} + create_timer(1740.0).timeout.connect(func() -> void: finish("probe_deadline")) + if OS.get_environment("THE_TREE_RESTART_K") != "30" or role not in ["R-writer", "R-reader", "O-writer", "O-reader"]: + finish("invalid_environment") + return + # No await before main: the default autoload clock cannot process here. + if role == "R-writer": + var fixture := OS.get_environment("THE_TREE_P28_FIXTURE") + if FileAccess.get_sha256(fixture) != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"): + finish("fixture_sha") + return + var file := FileAccess.open(fixture, FileAccess.READ) + if file == null: + finish("fixture_missing") + return + var value: Variant = file.get_var(false) + file.close() + if not value is Dictionary or not runtime.restore_snapshot(value): + finish("fixture_restore") + return + main = load("res://scenes/main/main.tscn").instantiate() + root.add_child(main) + report.main_added = true + report["startup"] = {"start_report": main.get("_operations_start_report").duplicate(true), + "operations": runtime.get_operations_report(), "clock": clock_observation()} + if role != "R-writer" and not GameClock.paused: + finish("main_did_not_pause") + return + var start_result: Dictionary = report.startup.start_report + var operations_enabled: Variant = field(report.startup.operations, "enabled") + if role == "R-writer": + if operations_enabled != false or field(start_result, "code") != "INVALID_START_TIME": + finish("unexpected_R_start") + return + elif operations_enabled != true or field(start_result, "ok") != true or GameClock.absolute_minute != 360: + finish("unexpected_fresh_start") + return + runtime.save_operation_completed.connect(on_save) + GameClock.tick_completed.connect(on_tick) + GameClock.simulation_overloaded.connect(on_overload) + start_minute = GameClock.absolute_minute + report["start_minute"] = start_minute + if role.ends_with("writer"): + main.call("_set_speed", 1) + while not GameClock.paused and not finished: + await process_frame + if finished: + return + if not selected_tick and not overloaded: + finish("unexpected_ui_stop") + return + if role == "O-writer" and auto_ids().is_empty(): + report["boundary"] = null + report["boundary_reason"] = "no_arrival_within_cap" if GameClock.absolute_minute - start_minute >= 120 else "stopped_before_arrival" + finish(report.boundary_reason) + return + if not writer_save(): + finish("save_comparison_failed") + return + else: + if not import_transfer(): + finish("transfer_failed") + return + if not reader_load(): + finish("load_comparison_failed") + return + report["event_id"] = E + report["saved_minute"] = GameClock.absolute_minute + boundary = operations_observation() if role.begins_with("O-") else null + report["boundary"] = boundary + var idle_before: Dictionary = runtime.create_snapshot() + var clock_before := clock_observation() + await process_frame + await process_frame + if finished: + return + var idle_after: Dictionary = runtime.create_snapshot() + report["paused_idle"] = {"before": clock_before, "after": clock_observation(), + "comparison": compare(idle_before, idle_after)} + if not report.paused_idle.comparison.equal or report.paused_idle.before != report.paused_idle.after: + finish("paused_backlog_consumed") + return + io_ready = true + for _index in range(30): + if not deterministic_step(): + finish("no_tick_advance_or_invalid_state") + return + # Keep the event loop responsive to deadline/audio while remaining paused. + await process_frame + if finished: + return + if not record_state("final", runtime.create_snapshot()): + finish("state_write_failed") + return + report["final_minute"] = GameClock.absolute_minute + if role.begins_with("O-"): + var last := operations_observation() + var added: Array = [] + for minute: Variant in last.processed_arrival_minutes: + if minute not in boundary.processed_arrival_minutes: + added.append(minute) + report["next_boundary"] = {"minute": added[0] if added.size() == 1 else null, + "processed_once": added.size() == 1, "before": boundary.processed_arrival_minutes, + "after": last.processed_arrival_minutes, "arrival_reports": last.arrival_reports, + "guest_auto_ids": last.guest_auto_ids} + if added.size() != 1: + finish("next_arrival_boundary_missing_or_duplicate") + return + finish("complete") + +func io_proof(base: Dictionary, after: Dictionary, kind: String, returned: Variant) -> bool: + var row := {"kind": kind, "signal_count": signals.size(), + "return_kind": "bool" if kind == "save" else "void", "return_value": returned} + report.io.append(row) + if signals.size() != 1: + return false + var signal_result: Dictionary = signals[0] + var payload: Dictionary = runtime.call("_save_event_payload", signal_result) + var latest: Dictionary = runtime.latest_save_operation_result.duplicate(true) + row["signal_projection"] = "_save_event_payload" + row["signal_payload"] = payload + row["latest"] = latest + row["payload_bytes_equal"] = var_to_bytes(payload) == var_to_bytes(latest) + var before := witness(base) + var after_witness := witness(after) + var eid := "domain_event.%012d" % (int(before.counter) + 1) + var found: Array = [] + for event: Dictionary in after_witness.recent_events: + if field(event, "event_id") == eid: + found.append(event) + if found.size() != 1 or before.published_ids.has(eid): + return false + var event: Dictionary = found[0] + row["event"] = event + row["before"] = before + row["after"] = after_witness + row["minute_before"] = field(field(base, "clock"), "absolute_minute") + row["minute_after"] = field(field(after, "clock"), "absolute_minute") + row["paused_before"] = field(field(base, "clock"), "paused") + row["paused_after"] = field(field(after, "clock"), "paused") + row["dropped"] = before.recent_events[0] if before.recent_events.size() == 256 else null + var expected := base.duplicate(true) + var part := runtime_part(expected) + var counters: Dictionary = field(field(part, "id_generator"), "counters") + counters["domain_event"] = int(before.counter) + 1 + var events: Dictionary = field(part, "domain_events") + var ids: Array = field(events, "published_ids") + ids.append(eid) + ids.sort() + var recent: Array = field(events, "recent_events") + if recent.size() > 256: + return false + if recent.size() == 256: + recent.pop_front() + recent.append(event.duplicate(true)) + row["comparison"] = compare(expected, after) + var expected_type := {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", "tamper": "SAVE_LOAD_FAILED"} + var ok: bool = row.payload_bytes_equal and row.comparison.equal and \ + var_to_bytes(field(event, "payload")) == var_to_bytes(payload) and \ + field(event, "event_type") == expected_type[kind] and field(event, "source_service") == "save_service" and \ + field(event, "entity_ids").is_empty() and field(event, "causation_command_id") == 0 and \ + row.minute_before == row.minute_after and field(event, "absolute_minute") == row.minute_after + if kind == "tamper": + var recovery: Variant = field(signal_result, "recovery_report") + var migration: Variant = field(signal_result, "migration_attempted") + row["failure"] = {"preload_pause_restored": field(signal_result, "preload_pause_restored"), + "loaded_clock_paused": field(signal_result, "loaded_clock_paused"), "recovery_report": recovery, + "migration_attempted": migration == true} + failure_event = event.duplicate(true) + return ok and field(payload, "ok") == false and field(payload, "code") != "OK" and \ + row.failure.preload_pause_restored == true and row.failure.loaded_clock_paused == row.paused_before and \ + row.paused_before == row.paused_after and (recovery == null or field(recovery, "restore_committed") == false) + E = eid + return ok and field(payload, "ok") == true and field(payload, "code") == "OK" and \ + (returned == true if kind == "save" else returned == null) and row.paused_after == true and \ + field(payload, "saved_absolute_minute") == row.minute_after + +func writer_save() -> bool: + var before: Dictionary = runtime.create_snapshot() + signals = [] + var returned: bool = main.call("_save_manual_at_path", "user://p28_restart/good", "P28 RESTART") + var after: Dictionary = runtime.create_snapshot() + var loaded := SaveService.load_snapshot("user://p28_restart/good") + if field(loaded, "ok") != true: + return false + package = field(loaded, "snapshot") + report["package_matches_before"] = compare(before, package) + if not record_state("P", package) or not record_state("after_save", after): + return false + return report.package_matches_before.equal and io_proof(package, after, "save", returned) + +func import_transfer() -> bool: + var transfer := OS.get_environment("THE_TREE_RESTART_TRANSFER") + var manifest_path := OS.get_environment("THE_TREE_RESTART_TRANSFER_MANIFEST") + if not transfer.is_absolute_path() or not manifest_path.is_absolute_path(): + return false + var manifest: Variant = JSON.parse_string(FileAccess.get_file_as_string(manifest_path)) + if not manifest is Dictionary: + return false + for kind: String in ["good", "tampered"]: + var target := "user://p28_restart/" + kind + if DirAccess.dir_exists_absolute(target): + return false + for relative: String in manifest.files[kind]: + if relative.is_absolute_path() or ".." in relative.split("/") or "\\" in relative: + return false + var source := transfer.path_join(kind).path_join(relative) + var expected: Dictionary = manifest.files[kind][relative] + if FileAccess.get_sha256(source) != expected.sha256: + return false + var data := FileAccess.get_file_as_bytes(source) + if data.size() != int(expected.bytes): + return false + var destination := target.path_join(relative) + if DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(destination).get_base_dir()) != OK: + return false + var file := FileAccess.open(destination, FileAccess.WRITE) + if file == null: + return false + file.store_buffer(data) + file.close() + if FileAccess.get_sha256(destination) != expected.sha256: + return false + report["transfer_manifest_sha256"] = FileAccess.get_sha256(manifest_path) + report["transfer_verified"] = true + return true + +func reader_load() -> bool: + var loaded := SaveService.load_snapshot("user://p28_restart/good") + if field(loaded, "ok") != true: + return false + package = field(loaded, "snapshot") + var before: Dictionary = runtime.create_snapshot() + if not record_state("P", package) or not record_state("B", before): + return false + signals = [] + main.call("_load_manual_at_path", "user://p28_restart/tampered", true) + var failed: Dictionary = runtime.create_snapshot() + if not record_state("after_tamper", failed) or not io_proof(before, failed, "tamper", null): + return false + signals = [] + main.call("_load_manual_at_path", "user://p28_restart/good", true) + var after: Dictionary = runtime.create_snapshot() + if not record_state("after_load", after) or not io_proof(package, after, "load", null): + return false + report["failure_event_absent"] = true + for event: Dictionary in witness(after).recent_events: + if var_to_bytes(event) == var_to_bytes(failure_event): + report.failure_event_absent = false + return report.failure_event_absent + +func deterministic_step() -> bool: + if not io_ready or not GameClock.paused: + return false + var before: int = GameClock.absolute_minute + var backlog: int = GameClock.get_backlog_minutes() + GameClock.advance_minutes(1) + var after: int = GameClock.absolute_minute + if after != before + 1: + return false + var state: Dictionary = runtime.create_snapshot() + var normalized := state.duplicate(true) + var recent: Array = field(field(runtime_part(normalized), "domain_events"), "recent_events") + var replacement: Variant = null + var count := 0 + var recent_ids: Array = [] + for index in range(recent.size()): + recent_ids.append(field(recent[index], "event_id")) + if field(recent[index], "event_id") == E: + replacement = recent[index].duplicate(true) + recent[index] = {"event_id": E, "replaced_for_cross_pid_compare": true} + count += 1 + if count > 1: + return false + report.minutes.append({"before": before, "after": after, "paused": GameClock.paused, + "backlog_before": backlog, "backlog_after": GameClock.get_backlog_minutes(), + "sha256": bytes_hash(var_to_bytes(normalized)), "scope": "complete_typed_var_to_bytes", + "excluded_paths": [], "replacement": replacement, "replacement_count": count, + "recent_count": recent.size(), "recent_ids": recent_ids}) + report.actual_k += after - before + return GameClock.paused and backlog == GameClock.get_backlog_minutes() + +func finish(reason: String) -> void: + if finished: + return + finished = true + report["stop_reason"] = reason + report["key_forms"] = key_forms + print("P28_RESTART_REPORT=" + JSON.stringify(report)) + if is_instance_valid(main) and main.is_inside_tree(): + main.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT) + get_root().close_requested.emit() + else: + quit(1) + +func emit_audio_close(result: Dictionary) -> void: + print("P28_RESTART_AUDIO_CLOSE=" + JSON.stringify(result)) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py new file mode 100644 index 0000000..cf16653 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +"""Engine-free positive and adversarial controls. No native acceptance claimed.""" +import copy +import hashlib +import json +import os +from pathlib import Path +import sys +import tempfile +import time +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +sys.dont_write_bytecode = True +import check_restart as check +import restart_pair as driver + +DATA = b"synthetic typed bytes fixture (not engine evidence)" +HASH = hashlib.sha256(DATA).hexdigest() + + +def comparison(): + return {"method": "var_to_bytes", "equal": True, "diff_paths": [], "excluded_paths": [], + "expected_sha256": HASH, "actual_sha256": HASH} + + +def history(count=256): + ids = ["domain_event.%012d" % n for n in range(1, count + 1)] + return {"counter": count, "published_ids": ids, + "recent_events": [{"event_id": eid, "event_type": "OLD", "payload": {"n": n}} + for n, eid in enumerate(ids)]} + + +def io(kind, minute=612, count=256): + before = history(count) + payload = {"ok": kind != "tamper", "code": "OK" if kind != "tamper" else "CORRUPT_WORLD", + "operation": "save" if kind == "save" else "load", + "save_path": "user://p28_restart/" + ("tampered" if kind == "tamper" else "good"), + "save_id": "synthetic", "saved_absolute_minute": minute} + event = {"event_id": check.event_id(count), + "event_type": {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", + "tamper": "SAVE_LOAD_FAILED"}[kind], + "source_service": "save_service", "entity_ids": [], "causation_command_id": 0, + "absolute_minute": minute, "payload": copy.deepcopy(payload)} + return {"kind": kind, "signal_count": 1, "signal_payload": payload, "latest": copy.deepcopy(payload), + "event": event, "signal_projection": "_save_event_payload", "payload_bytes_equal": True, + "return_kind": "bool" if kind == "save" else "void", + "return_value": True if kind == "save" else None, + "before": before, "after": check.transform(before, event), + "dropped": before["recent_events"][0] if count == 256 else None, + "comparison": comparison(), "minute_before": minute, "minute_after": minute, + "paused_before": True, "paused_after": True, + "failure": {"preload_pause_restored": True, "loaded_clock_paused": True, + "migration_attempted": False, "recovery_report": {"restore_committed": False}}} + + +def report(role="R-writer", pid=100): + writer, ops = role.endswith("writer"), role.startswith("O-") + minute = 391 if ops else 612 + rows = [io("save", minute)] if writer else [io("tamper", 360, 3), io("load", minute)] + names = ["P", "after_save", "final"] if writer else ["P", "B", "after_tamper", "after_load", "final"] + event = rows[-1]["event"] + boundary = {"clock": minute, "guest_auto_ids": ["guest-auto-00000001"], "guest_auto_count": 1, + "operations": {"enabled": True}, "processed_arrival_minutes": [390], + "arrival_reports": [{"minute": 390}]} if ops else None + next_boundary = {"minute": 420, "processed_once": True, "before": [390], "after": [390, 420], + "arrival_reports": [{"minute": 390}, {"minute": 420}], + "guest_auto_ids": ["guest-auto-00000001", "guest-auto-00000002"]} if ops else None + clock = {"minute": minute, "paused": True, "backlog": 121, "accumulator_hex": "00"} + return {"schema": "p28-restart-v1", "role": role, "pid": pid, "stop_reason": "complete", + "main_added": True, "k_target": 30, "actual_k": 30, "comparison_scope": "complete_typed_var_to_bytes", + "excluded_paths": [], "transfer_before_guard_writes": 0, "probe_deadline_seconds": 1740, + "io": rows, "package_matches_before": comparison(), "failure_event_absent": True, + "transfer_verified": True, "event_id": event["event_id"], "saved_minute": minute, + "start_minute": 360 if ops else 602, "final_minute": minute + 30, + "states": {n: {"path": "p28_restart/evidence/" + n + ".bin", "bytes": len(DATA), "sha256": HASH} + for n in names}, + "paused_idle": {"before": clock, "after": copy.deepcopy(clock), "comparison": comparison()}, + "boundary": boundary, "next_boundary": next_boundary, "ui_ticks": [copy.deepcopy(boundary)] if ops else [], + "minutes": [{"before": minute + n, "after": minute + n + 1, "paused": True, + "backlog_before": 121, "backlog_after": 121, "sha256": HASH, + "scope": "complete_typed_var_to_bytes", "excluded_paths": [], + "replacement": copy.deepcopy(event) if n < 5 else None, + "replacement_count": 1 if n < 5 else 0, "recent_count": 256, + "recent_ids": history(255)["published_ids"] + + ([event["event_id"]] if n < 5 else ["domain_event.000000000256"])} + for n in range(30)]} + + +def write(path, value): + with Path(path).open("x") as stream: + json.dump(value, stream) + + +def audio(pid): + return {"coordinator": True, "passed": True, "owned_engine_pid": pid, + "all_owned_targets_released": True, "deadline_reached": False, + "clock_process_held_stopped": True, "window_input_held_disabled": True, + "restored_before_quit": False, "shared_bus_deleted": False, "settings_mutated": False} + + +def synthetic_receipt(out): + rows = [] + profiles = {} + source_package = out / "synthetic-package" + source_package.mkdir() + (source_package / "world.bin").write_bytes(DATA) + (source_package / "manifest.json").write_text("{}") + transfers = {kind: driver.create_transfer(source_package, out / (kind + "-transfer"), write) + for kind in ("R", "O")} + for index, label in enumerate(check.PHASES): + folder = out / label + folder.mkdir() + pid = 100 + index + process = {"exit_code": 0, "helper_exit_code": 0, "timed_out": False, "error_lines": [], + "lifecycle_error": None, "external_process_or_group_signals": 0, + "owned_popen_ended": True, "old_helper_engine_ended": True, + "start_receipt_written": True, "end_receipt_written": True} + capture = {"passed": True, "all_captured_ended": True, "actual_captured_pids": [pid], + "outer_cleanup_errors": [], "adapter_exception": None, "foreign_process_or_group_signals": 0, + "all_cleanup_before_outer_receipt": True, "outer_receipt_written": True, + "outer_receipt_error": None, "cleanup": []} + write(folder / "outer-owned-finally.json", {k: v for k, v in capture.items() if k not in + ("passed", "outer_receipt_written", "outer_receipt_error")}) + for filename, key in (("started.json", "actual_start_binding"), ("ended.json", "actual_end_binding")): + binding = {"pid": pid, "actual_pid": pid, "owner_pid": 99, + "argv": ["fake", "--check-only"], "command": ["fake", "--check-only"]} + write(folder / filename, binding) + process[key] = binding + process["adapter_owned_cleanup"] = {"engine_ended": True, "errors": []} + write(folder / "adapter-owned-cleanup.json", process["adapter_owned_cleanup"]) + process.update(pid=pid, argv=["fake", "--check-only"]) + raw = "" + row = {"phase": label, "passed": True, "process": process, "capture": capture, + "timeout_seconds": 1800, "diagnostics": [], "argv": ["fake", "--check-only"], + "stage_before": HASH, "stage_after": HASH, "stage_preserved": True, + "source_preserved": True, "player_preserved": True} + startup = out / (label + "-startup") + startup.write_bytes(DATA) + row["startup_project"] = {"fresh_before": True, "path": str(startup), "sha256": HASH} + if index >= 3: + observed = report(label, pid) + if label.endswith("reader"): + observed["transfer_manifest_sha256"] = check.sha(out / (label[0] + "-transfer/manifest.json")) + row["report"] = observed + raw = "P28_RESTART_REPORT=" + json.dumps(observed) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(pid)) + "\n" + profile = str(out / (label + "-private")) + row.update(runtime_profile=profile, runtime_name=label) + profiles[profile] = {} + for kind in (("good",) if label.endswith("writer") else ("good", "tampered")): + for name, value in transfers[label[0]]["files"][kind].items(): + profiles[profile]["p28_restart/" + kind + "/" + name] = value + guard = {"verified": True, "conditions": [True] * 5, "conditions_checked": 5, + "actual_user_data_dir": profile, "expected_user_data_dir": profile, + "globalized_probe": str(Path(profile) / "guard-probe"), "custom_user_dir_name": label, + "fresh_before_parent_initialize": True, "profile_writes_before_guard": 0, + "project_settings_persisted": False} + guard_path = out / (label + "-guard.json") + write(guard_path, guard) + row.update(guard=guard, guard_sha256=check.sha(guard_path)) + states = out / (label + "-states") + states.mkdir() + for name in observed["states"]: + (states / (name + ".bin")).write_bytes(DATA) + for name, content in (("stdout.log", raw), ("stderr.log", ""), ("engine-native.log", "native\n")): + (folder / name).write_text(content) + row["raw"] = {n: check.pin(folder / n) for n in ("stdout.log", "stderr.log", "engine-native.log")} + process.update(stdout_sha256=row["raw"]["stdout.log"]["sha256"], + stderr_sha256=row["raw"]["stderr.log"]["sha256"]) + write(folder / "process.json", process) + row["owned_files"] = {n: check.pin(folder / n) for n in + ("process.json", "started.json", "ended.json", "adapter-owned-cleanup.json", "outer-owned-finally.json")} + rows.append(row) + probe = out / "restart_probe.gd" + probe.write_bytes((check.HERE / probe.name).read_bytes()) + source = {str(i): {"bytes": 1, "sha256": HASH} for i in range(1407)} + worker = {n: check.sha(check.HERE / n) for n in + ("restart_pair.py", "restart_probe.gd", "check_restart.py", "test_restart.py")} + return {"passed": True, "error": None, "cleanup_errors": [], "phases": rows, + "source_before": source, "source_after": copy.deepcopy(source), + "default_player_before": {}, "default_player_after": {}, + "dependencies_before": check.DEPENDENCIES.copy(), "dependencies_after": check.DEPENDENCIES.copy(), + "collector_before": driver.COLLECTOR_PINS.copy(), "collector_after": driver.COLLECTOR_PINS.copy(), + "worker_before": worker, "worker_after": worker.copy(), "profiles": profiles, + "cross_cutting": copy.deepcopy(check.CROSS), "original_credit": 0, + "windows_verified": False, "operations_200_guests_verified": False, + "script_pins": {str(probe): check.sha(probe)}, "owned_writes": [], + "cleanup": [{"pid": 100 + n, "engine_ended": True, "outer_cleanup_errors": [], + "foreign_process_or_group_signals": 0} for n in range(7)]} + + +class RestartTests(unittest.TestCase): + def rejects(self, original, mutations, validator): + for name, mutation in mutations.items(): + with self.subTest(rejection=name): + value = copy.deepcopy(original) + mutation(value) + with self.assertRaises((ValueError, KeyError, TypeError)): + validator(value) + + def test_rs1_signal_bool_void(self): + for kind in ("save", "load", "tamper"): + check.check_io(io(kind), kind) + self.rejects(io("save"), { + "true only": lambda x: x.pop("signal_count"), + "no signal": lambda x: x.update(signal_count=0), + "duplicate signal": lambda x: x.update(signal_count=2), + "bool differs": lambda x: x.update(return_value=False), + "latest differs": lambda x: x["latest"].update(code="FAILED"), + "payload typed mismatch": lambda x: x.update(payload_bytes_equal=False), + "state mismatch": lambda x: x["comparison"].update(equal=False), + }, lambda x: check.check_io(x, "save")) + self.rejects(io("load"), { + "void success": lambda x: x.update(return_value=True), + "void coerced bool": lambda x: x.update(return_kind="bool"), + "missing signal": lambda x: x.update(signal_count=0), + }, lambda x: check.check_io(x, "load")) + + def test_rs2_exact_event_transform(self): + for kind in ("save", "load", "tamper"): + for count in (0, 255, 256): + check.check_io(io(kind, count=count), kind) + self.rejects(io(kind), { + "two events": lambda x: x["after"]["recent_events"].append(copy.deepcopy(x["event"])), + "different E": lambda x: x["event"].update(event_id="domain_event.000009999999"), + "counter": lambda x: x["after"].update(counter=x["before"]["counter"] + 2), + "missing ID": lambda x: x["after"]["published_ids"].pop(0), + "duplicate ID": lambda x: x["after"]["published_ids"].append(x["event"]["event_id"]), + "unsorted IDs": lambda x: x["after"]["published_ids"].reverse(), + "drop tail": lambda x: x["after"].update(recent_events=x["before"]["recent_events"][:-1] + [x["event"]]), + "wrong dropped record": lambda x: x.update(dropped=x["before"]["recent_events"][-1]), + "excluded path": lambda x: x["comparison"].update(excluded_paths=["systems.runtime.economy"]), + "diff hidden": lambda x: x["comparison"].update(diff_paths=["$.clock"]), + "digest only": lambda x: x["comparison"].update(method="SimulationDigest"), + "bytes SHA disagreement": lambda x: x["comparison"].update(actual_sha256="f" * 64), + }, lambda x: check.check_io(x, kind)) + + def test_clarification01_tamper(self): + self.rejects(io("tamper"), { + "failure event ignored": lambda x: x.update(after=x["before"]), + "failure ok": lambda x: x["signal_payload"].update(ok=True), + "failure OK code": lambda x: x["signal_payload"].update(code="OK"), + "not restored": lambda x: x["failure"].update(preload_pause_restored=False), + "paused differs": lambda x: x.update(paused_after=False), + "loaded pause differs": lambda x: x["failure"].update(loaded_clock_paused=False), + "restore committed": lambda x: x["failure"]["recovery_report"].update(restore_committed=True), + "migration unrecorded": lambda x: x["failure"].pop("migration_attempted"), + }, lambda x: check.check_io(x, "tamper")) + self.rejects(report("R-reader"), { + "failed event retained": lambda x: x.update(failure_event_absent=False), + "backlog consumed": lambda x: x["paused_idle"]["after"].update(backlog=120), + "reader not paused": lambda x: x["io"][1].update(paused_after=False), + }, check.check_report) + + def test_rs3_actual_k_full_typed_cross_pid(self): + writer, reader = report(), report("R-reader", 101) + check.check_pair(writer, reader) + self.rejects(reader, { + "K call count": lambda x: x["minutes"][0].update(after=x["saved_minute"]), + "different K": lambda x: x.update(actual_k=29), + "missing minute": lambda x: x["minutes"].pop(), + "partial digest": lambda x: x["minutes"][0].update(scope="SimulationDigest"), + "minute SHA": lambda x: x["minutes"][0].update(sha256="f" * 64), + "wrong replacement": lambda x: x["minutes"][0]["replacement"].update(event_id="other"), + "two replacements": lambda x: x["minutes"][0].update(replacement_count=2), + "after window": lambda x: x["minutes"][6].update(replacement=x["io"][-1]["event"], replacement_count=1), + "excluded path": lambda x: x["minutes"][0].update(excluded_paths=["clock"]), + "same PID": lambda x: x.update(pid=100), + }, lambda x: check.check_pair(writer, x)) + + def test_rs4_rs5_arrival_boundary(self): + writer, reader = report("O-writer"), report("O-reader", 101) + check.check_pair(writer, reader) + self.rejects(writer, { + "boundary only": lambda x: x["boundary"].update(guest_auto_count=0), + "null arrival": lambda x: x.update(boundary=None), + "unprocessed": lambda x: x["boundary"].update(processed_arrival_minutes=[]), + "not first tick": lambda x: x["ui_ticks"].insert(0, copy.deepcopy(x["boundary"])), + "duplicate guest": lambda x: x["boundary"]["guest_auto_ids"].append("guest-auto-00000001"), + "next missing": lambda x: x["next_boundary"].update(processed_once=False), + "next duplicate": lambda x: x["next_boundary"]["after"].append(420), + "cap exceeded": lambda x: x.update(start_minute=200), + }, check.check_report) + + def test_d1_import_closure(self): + check.check_dependencies() + check.check_modules() + for name in ("restart_pair.py", "check_restart.py", "test_restart.py"): + check.check_imports((check.HERE / name).read_text()) + for text in ("import forbidden_project_module", "from tools.strict_test_markers import main", + "importlib.util.spec_from_file_location('x', 'tools/evil.py')", + "__import__('tools.evil')", "exec('import tools.evil')"): + with self.subTest(text=text), self.assertRaises(ValueError): + check.check_imports(text) + with self.assertRaises(ValueError): + check.check_modules({"bad": SimpleNamespace(__file__=str(check.ROOT / "tools/evil.py"))}) + for path in check.DEPENDENCIES: + original = check.sha + with self.subTest(pin=path), patch.object(check, "sha", side_effect=lambda p: + "0" * 64 if Path(p) == check.ROOT / path else original(p)), self.assertRaises(ValueError): + check.check_dependencies() + + def test_d2_order_and_check_plan(self): + collector, _, _ = driver.load_dependencies() + commands = driver.plan(collector, Path("/engine"), Path("/out")) + self.assertEqual(len(commands), 7) + self.assertIn("--import", commands[0]) + self.assertIn("--check-only", commands[1]) + self.assertIn("--check-only", commands[2]) + rows = [] + for _, label in driver.schedule(rows): + rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}}) + self.assertEqual([r["phase"] for r in rows], check.PHASES) + rows = [] + iterator = driver.schedule(rows) + for _ in range(4): + _, label = next(iterator) + rows.append({"phase": label, "passed": True, "report": {"stop_reason": "complete"}}) + rows[-1]["passed"] = False + with self.assertRaises(ValueError): + next(iterator) + rows[-1]["passed"] = True + rows[-1]["report"]["stop_reason"] = "failed" + iterator = driver.schedule([]) + next(iterator) + with self.assertRaises(ValueError): + next(iterator) + + def test_d3_transfer_and_fresh_profiles(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + base = Path(temporary) + source = base / "source" + source.mkdir() + (source / "world.bin").write_bytes(b"0123456789") + (source / "manifest.json").write_text("{}") + target = base / "transfer" + manifest = driver.create_transfer(source, target, write) + driver.check_transfer(target, manifest) + self.assertEqual((target / "tampered/world.bin").read_bytes(), b"0123446789") + driver.fresh_profile(base / "new-profile") + with self.assertRaises(ValueError): + driver.fresh_profile(source) + self.rejects(manifest, { + "SHA": lambda x: x["files"]["good"]["world.bin"].update(sha256="f" * 64), + "offset": lambda x: x["tamper"].update(offset=0), + "xor": lambda x: x["tamper"].update(xor=2), + }, lambda x: driver.check_transfer(target, x)) + (target / "tampered/manifest.json").write_text("changed") + with self.assertRaises(ValueError): + driver.check_transfer(target, manifest) + + def test_d4_d5_d7_probe_static(self): + text = (check.HERE / "restart_probe.gd").read_text() + check.check_probe(text) + snippets = ["GameClock.advance_minutes(1)", "GameClock.set_paused(false)", + "x.clear()", "spawn_guest()", "SimulationRuntime.new()", "initialize()", + "enable_production_checkpoint()", "main._toggle_pause()", "GameClock.set_paused(true)", + "var elapsed_us = 1", "Time.get_ticks_usec()"] + for snippet in snippets: + with self.subTest(snippet=snippet), self.assertRaises(ValueError): + check.check_probe(text + "\nfunc bad():\n\t" + snippet + "\n") + for replacement in ("GameClock.advance_minutes(2)", "Time.get_ticks_usec()", + "GameClock.advance_one_minute()"): + with self.subTest(step=replacement), self.assertRaises(ValueError): + check.check_probe(text.replace("GameClock.advance_minutes(1)", replacement)) + + def test_rs7_rs8_full_receipt_rejections(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + check.check_receipt(receipt, out) + self.rejects(receipt, { + "source map": lambda x: x["source_after"].pop("0"), + "ordinary player": lambda x: x["default_player_after"].update(changed=True), + "guard": lambda x: x["phases"][3]["guard"].update(profile_writes_before_guard=1), + "raw SHA": lambda x: x["phases"][3]["raw"]["stdout.log"].update(sha256="f" * 64), + "PID": lambda x: x["phases"][3]["capture"].update(actual_captured_pids=[999]), + "owned write": lambda x: x["owned_writes"].append("R-writer/report.json"), + "cleanup": lambda x: x["cleanup"][0].update(engine_ended=False), + "stage": lambda x: x["phases"][1].update(stage_after="f" * 64), + "collector": lambda x: x["collector_after"].pop("measure_p28_01.gd"), + "dependency": lambda x: x["dependencies_after"].update({"tools/run_all_tests.py": "f" * 64}), + "cross-cutting": lambda x: x["cross_cutting"].pop(), + "source count": lambda x: (x["source_after"].pop("1"), x["source_before"].pop("1")), + "phase order": lambda x: x["phases"].reverse(), + "foreign signal": lambda x: x["phases"][0]["capture"].update(foreign_process_or_group_signals=1), + }, lambda x: check.check_receipt(x, out)) + (out / "R-reader-states/B.bin").write_bytes(b"changed") + with self.assertRaises(ValueError): + check.check_receipt(receipt, out) + + def test_d6_raw_report_and_audio(self): + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + row = receipt["phases"][3] + folder = out / "R-writer" + original = (folder / "stdout.log").read_text() + for text in ("", original + original, original.split("P28_RESTART_AUDIO_CLOSE=")[0], + original.replace('"owned_engine_pid": 103', '"owned_engine_pid": 999')): + (folder / "stdout.log").write_text(text) + row["raw"]["stdout.log"] = check.pin(folder / "stdout.log") + with self.assertRaises(ValueError): + check.bound_raw(folder, row) + + def test_d2_d6_real_owned_outer_fake_child(self): + collector, _, outer = driver.load_dependencies() + child = ( + "import os,sys,json,time,pathlib\n" + "mode=sys.argv[1]\n" + "identity={'pid':os.getpid(),'ppid':os.getppid(),'pgid':os.getpgrp()," + "'args':' '.join([sys.executable]+sys.argv)}\n" + "path=pathlib.Path(sys.argv[0]).parent/(mode+'-identity')\n" + "tmp=path.with_suffix('.tmp'); tmp.write_text(json.dumps(identity)); tmp.replace(path)\n" + "pathlib.Path(sys.argv[sys.argv.index('--log-file')+1]).write_text('native raw\\n')\n" + "print('Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org',flush=True)\n" + "time.sleep(0.15)\n" + "if mode=='timeout': time.sleep(5)\n" + "sys.exit(3 if mode=='exit' else 0)\n") + with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + base = Path(temporary) + script = base / "fake-child" + script.write_text(child) + def diagnostic(command, folder, timeout, env, **kwargs): + def probe(pid): + path = base / (command[2] + "-identity") + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if path.exists(): + identity = json.loads(path.read_text()) + if identity["pid"] == pid: + return identity + time.sleep(.005) + raise RuntimeError("fake child identity unavailable") + return outer.native.run_owned_diagnostic(command, folder, timeout, env, probe_fn=probe, **kwargs) + for mode in ("ok", "exit", "timeout"): + folder = base / mode + def run_phase(command, destination, limit, env, **kwargs): + self.assertEqual(limit, 1800) + return outer.run_phase(command, destination, .3 if mode == "timeout" else 5, + env, diagnostic_fn=diagnostic, **kwargs) + def guarded_write(path, value): + self.assertFalse(Path(path).is_relative_to(folder), "driver writes owned folder") + write(path, value) + def real_phase(*args, **kwargs): + kwargs["dump_fn"] = write + return run_phase(*args, **kwargs) + with patch.object(collector, "live_engines", return_value=[]), \ + patch.object(collector, "write", side_effect=guarded_write): + row = collector.run_engine(SimpleNamespace(run_phase=real_phase), + [sys.executable, str(script), mode], folder, {}) + row["phase"] = "import" + self.assertEqual(row["passed"], mode == "ok", row) + self.assertTrue(row["capture"]["all_captured_ended"]) + if mode == "ok": + check.bound_raw(folder, row) + else: + with self.assertRaises(ValueError): + check.bound_raw(folder, row) + _, errors = outer.finish_captured(outer.OWNED) + self.assertEqual(errors, []) + + +if __name__ == "__main__": + unittest.main(verbosity=2) From bc703c9f6f82aa966328cf48134838eb7b73170d Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 01:57:18 +0900 Subject: [PATCH 2/5] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/test_restart.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py index cf16653..b551ad9 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -191,6 +191,24 @@ def synthetic_receipt(out): class RestartTests(unittest.TestCase): + @staticmethod + def here_inventory(): + return {str(path.relative_to(check.HERE)): hashlib.sha256(path.read_bytes()).hexdigest() + for path in check.HERE.rglob("*") if path.is_file()} + + @classmethod + def setUpClass(cls): + cls.inventory_before = cls.here_inventory() + + def setUp(self): + # Check after every test, including failures, independently of test order. + self.addCleanup(lambda: self.assertEqual(self.here_inventory(), self.inventory_before)) + + def test_zz_here_inventory_unchanged(self): + # unittest's default ordering places this after all twelve existing tests. + self.assertIn("test_restart.py", self.inventory_before) + self.assertEqual(self.here_inventory(), self.inventory_before) + def rejects(self, original, mutations, validator): for name, mutation in mutations.items(): with self.subTest(rejection=name): @@ -328,7 +346,7 @@ class RestartTests(unittest.TestCase): next(iterator) def test_d3_transfer_and_fresh_profiles(self): - with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: base = Path(temporary) source = base / "source" source.mkdir() @@ -366,7 +384,7 @@ class RestartTests(unittest.TestCase): check.check_probe(text.replace("GameClock.advance_minutes(1)", replacement)) def test_rs7_rs8_full_receipt_rejections(self): - with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: out = Path(temporary) receipt = synthetic_receipt(out) check.check_receipt(receipt, out) @@ -391,7 +409,7 @@ class RestartTests(unittest.TestCase): check.check_receipt(receipt, out) def test_d6_raw_report_and_audio(self): - with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: out = Path(temporary) receipt = synthetic_receipt(out) row = receipt["phases"][3] @@ -418,7 +436,7 @@ class RestartTests(unittest.TestCase): "time.sleep(0.15)\n" "if mode=='timeout': time.sleep(5)\n" "sys.exit(3 if mode=='exit' else 0)\n") - with tempfile.TemporaryDirectory(dir=check.HERE) as temporary: + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: base = Path(temporary) script = base / "fake-child" script.write_text(child) From 5e4d96d9100cc1c0a7cea95a06b7e97d791d542f Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 02:55:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 268 +++++++++++++- .../p28-01/restart/probe/restart_pair.py | 16 +- .../p28-01/restart/probe/restart_probe.gd | 156 +++++++- .../p28-01/restart/probe/test_restart.py | 334 +++++++++++++++++- 4 files changed, 739 insertions(+), 35 deletions(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py index 04e8371..939d6d8 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -5,6 +5,7 @@ import copy import hashlib import importlib.util import json +import math from pathlib import Path import re import sys @@ -23,6 +24,13 @@ DEPENDENCIES = { } LOCAL = {BASE + "restart/probe/" + n for n in ("restart_pair.py", "check_restart.py", "test_restart.py", "restart_probe.gd")} +SIDECAR = "p28_restart/evidence/report_full.json" +TIMINGS = ("duration_us", "runtime_duration_us") +SAVE_EVENTS = { + "SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SAVE_LOADED", "SAVE_LOAD_FAILED", + "SAVE_MIGRATION_SUCCEEDED", "SAVE_MIGRATION_FAILED", "SAVE_RECOVERY_LOADED", + "SAVE_RECOVERY_CANDIDATE_LOADED", "SAVE_RECOVERY_FAILED", +} PHASES = ["import", "parse-base", "parse-wrapper", "R-writer", "R-reader", "O-writer", "O-reader"] CROSS = [ {"condition": "原本条件とUI操作が一致", "applicable": False, "reference": "collector C1/C2", @@ -120,6 +128,7 @@ def check_probe(text): function = None blocks = [] pauses = 0 + watchdog_calls = 0 for raw in text.splitlines(): line = raw.split("#", 1)[0] match = re.match(r"^func (\w+)\(", line) @@ -137,9 +146,16 @@ def check_probe(text): if "set_paused(" in line: require(function == "on_tick" and line.strip() == "GameClock.set_paused(true)", "pause outside tick") pauses += 1 - require(not re.search(r"Time\.get_ticks_usec|\b\w+_us\b|Performance\.|get_performance_report", line), + if "Time." in line: + require(function == "deadline_timer_seconds" and + line.strip() == "return Time.get_ticks_msec() / 1000.0", "timing API outside watchdog") + watchdog_calls += 1 + # Literal names are used only by the finite evidence redaction routine. + timing_line = line.strip() == 'for key: String in ["duration_us", "runtime_duration_us"]:' + require(not re.search(r"\b\w+_(?:us|ms)\b|Performance\.|get_performance_report", line) + or (function == "redact_payload" and timing_line), "performance instrumentation") - require(len(blocks) == 1 and pauses == 1, "deterministic/tick functions required") + require(len(blocks) == 1 and pauses == 1 and watchdog_calls == 1, "deterministic/tick/watchdog functions required") block = "\n".join(blocks[0]) require(block.count("GameClock.advance_minutes(1)") == 1 and "io_ready" in block and "GameClock.paused" in block, "step admission") @@ -211,8 +227,180 @@ def check_io(row, kind): require(type(failure["migration_attempted"]) is bool, "migration observation") -def check_report(report): +def save_event(event): + return isinstance(event, dict) and event.get("source_service") == "save_service" and \ + event.get("event_type") in SAVE_EVENTS + + +def payload_paths(report): + """The complete W2 closure; no recursive payload-name exemption.""" + result = {} + for i, row in enumerate(report["io"]): + prefix = "/report/io/%d" % i + for name in ("signal_payload", "latest"): + result[prefix + "/" + name] = row[name] + events = [(prefix + "/event", row["event"]), (prefix + "/dropped", row["dropped"])] + for side in ("before", "after"): + events.extend((prefix + "/" + side + "/recent_events/%d" % j, event) + for j, event in enumerate(row[side]["recent_events"])) + for path, event in events: + if save_event(event): + result[path + "/payload"] = event["payload"] + for k, row in enumerate(report["minutes"]): + if save_event(row["replacement"]): + result["/report/minutes/%d/replacement/payload" % k] = row["replacement"]["payload"] + return result + + +def walk(value, path=""): + if isinstance(value, dict): + for key, child in value.items(): + pointer = path + "/" + key.replace("~", "~0").replace("/", "~1") + yield pointer, key, child + yield from walk(child, pointer) + elif isinstance(value, list): + for i, child in enumerate(value): + yield from walk(child, path + "/" + str(i)) + + +def redact_report(verbatim): + """Also validates the full sidecar report before producing its unique public form.""" + full = json.loads(json.dumps(verbatim)) + allowed = payload_paths(full) + for path, key, _ in walk(full, "/report"): + require(key != "timing_redactions", "reserved marker in verbatim evidence") + require(not key.endswith(("_us", "_ms")) or + (path.rsplit("/", 1)[0] in allowed and key in TIMINGS), "timing outside W2 closure") + require(key not in ("performance", "statistics", "fps", "budget_verdict"), + "sidecar performance use") + for path, payload in allowed.items(): + require(isinstance(payload, dict) and all(k in payload for k in TIMINGS), "both timing keys required") + payload["timing_redactions"] = [ + {"original_key": key, "sidecar_ref": path + "/" + key} for key in TIMINGS] + for key in TIMINGS: + del payload[key] + check_no_metrics(full) + return full + + +def resolve_pointer(sidecar, pointer): + require(pointer.startswith("/report/"), "pointer outside report") + value = sidecar + for part in pointer[1:].split("/"): + part = part.replace("~1", "/").replace("~0", "~") + value = value[int(part)] if isinstance(value, list) else value[part] + return value + + +def json_sha(value): + # JSON numbers are represented uniformly across Godot's JSON float parser + # and Python. Fixed decimal form is valid JSON; keys and strings remain verbatim. + def encode(v): + if isinstance(v, dict): + return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + encode(v[k]) + for k in sorted(v)) + "}" + if isinstance(v, list): + return "[" + ",".join(encode(x) for x in v) + "]" + if type(v) in (int, float): + require(math.isfinite(v), "nonfinite JSON") + return format(v, ".17f") + return json.dumps(v, ensure_ascii=False, separators=(",", ":"), allow_nan=False) + return hashlib.sha256(encode(value).encode()).hexdigest() + + +def canonical_event(event, eid): + return {"event_id": eid, "replaced_for_cross_pid_compare": True} \ + if event["event_id"] == eid else event + + +def check_sidecar(public, sidecar): + require(isinstance(sidecar, dict) and set(sidecar) == {"report", "at_390_event_verbatim"}, + "sidecar root/missing") + check_no_metrics(public) + full = sidecar["report"] + require(public == redact_report(full), "public redaction differs from W2") + restored = copy.deepcopy(public) + for path, payload in payload_paths(restored).items(): + expected = [{"original_key": k, "sidecar_ref": path + "/" + k} for k in TIMINGS] + require(payload.pop("timing_redactions") == expected, "redaction marker/pointer mismatch") + for marker in expected: + payload[marker["original_key"]] = resolve_pointer(sidecar, marker["sidecar_ref"]) + require(restored == full, "entire restored report differs") + events = sidecar["at_390_event_verbatim"] + require(isinstance(events, list), "at390 verbatim array") + observation = full.get("at_390") + summaries = observation["events"] if observation is not None else [] + require(len(events) == len(summaries), "at390 verbatim count") + for event, summary in zip(events, summaries): + for path, key, _ in walk(event): + require(key != "timing_redactions" and key not in + ("performance", "statistics", "fps", "budget_verdict"), "at390 forbidden field") + require(not key.endswith(("_us", "_ms")) or + (save_event(event) and path == "/payload/" + key and key in TIMINGS), + "at390 timing outside SAVE payload") + if save_event(event): + require(all(k in event["payload"] for k in TIMINGS), "at390 both timing keys") + require(set(summary) == {"event_id", "event_type", "absolute_minute", + "raw_event_sha256", "canonical_sha256"}, "at390 summary shape") + require(all(event[k] == summary[k] for k in ("event_id", "event_type", "absolute_minute")), + "at390 verbatim order/metadata") + require(json_sha(event) == summary["raw_event_sha256"] and + json_sha(canonical_event(event, full["event_id"])) == summary["canonical_sha256"], + "at390 raw/canonical SHA") + return full + + +def check_deadline(value, complete=True): + require(set(value) == {"limit_seconds_from_engine_start", "engine_elapsed_at_begin_seconds", + "timer_seconds"}, "deadline shape") + elapsed, timer = value["engine_elapsed_at_begin_seconds"], value["timer_seconds"] + require(type(elapsed) in (int, float) and type(timer) in (int, float) and + math.isfinite(elapsed) and math.isfinite(timer) and elapsed >= 0 and + value["limit_seconds_from_engine_start"] == 1740 and timer == 1740 - elapsed, + "engine elapsed deadline formula") + require(not complete or timer > 0, "expired deadline") + + +def overload_case(report): + backlog = report["paused_idle"]["before"]["backlog"] + overloads = report["overloads"] + require(isinstance(overloads, list) and type(backlog) is int and backlog >= 0, "overload facts") + expected = {"exercised": bool(overloads and backlog > 0), + "overloads": len(overloads), "backlog_at_save": backlog} + require(report["overload_case"] == expected and + type(report["overload_case"]["exercised"]) is bool and + set(report["overload_case"]) == set(expected), "overload disclosure") + for _, key, value in walk(report): + if key in ("nonzero_backlog_preserved", "overload_persistence", + "across_restart_backlog_persistence_claimed"): + require(value is False, "unsupported overload/persistence claim") + return expected + + +def coverage(reports): + detail = {r["role"]: overload_case(r) for r in reports if r["role"].endswith("writer")} + return {"overload_nonzero_backlog": + "exercised_recorded" if detail and all(x["exercised"] for x in detail.values()) else "not_exercised", + "per_role": detail, "across_restart_backlog_persistence_claimed": False} + + +def check_no_arrival_diagnostic(report): + require(report["role"] == "O-writer" and report["boundary"] is None and + report["boundary_reason"] == report["stop_reason"] == "no_arrival_within_cap", + "no-arrival reason/null") + require(report["ui_ticks"] and + report["ui_ticks"][-1]["clock"] - report["start_minute"] == 120 and + all(t["guest_auto_count"] == 0 and t["guest_auto_ids"] == [] for t in report["ui_ticks"]), + "no-arrival cap/facts") + return True + + +def check_report(report, sidecar=None): check_no_metrics(report) + require(sidecar is not None, "sidecar required") + report = check_sidecar(report, sidecar) + if report["stop_reason"] == "no_arrival_within_cap": + check_no_arrival_diagnostic(report) require(report["schema"] == "p28-restart-v1" and report["stop_reason"] == "complete", "incomplete probe") require(report["role"] in PHASES[3:] and type(report["pid"]) is int and report["pid"] > 0, "role/PID") require(report["main_added"] is True and report["k_target"] == 30 and report["actual_k"] == 30, "main/K") @@ -220,12 +408,14 @@ def check_report(report): "partial digest/path exclusion") require(report["transfer_before_guard_writes"] == 0 and report["probe_deadline_seconds"] == 1740, "profile/deadline") + check_deadline(report["probe_deadline"]) writer = report["role"].endswith("writer") kinds = ["save"] if writer else ["tamper", "load"] require(len(report["io"]) == len(kinds), "IO sequence") for row, kind in zip(report["io"], kinds): check_io(row, kind) if writer: + overload_case(report) exact(report["package_matches_before"]) require(report["package_matches_before"]["actual_sha256"] == report["states"]["P"]["sha256"], "P not package snapshot") @@ -269,6 +459,32 @@ def check_report(report): require(report["states"]["after_tamper"]["sha256"] == report["io"][0]["comparison"]["actual_sha256"], "tamper state file differs") if report["role"].startswith("O-"): + observation = report["at_390"] + require(isinstance(observation, dict) and observation["clock"] == {"absolute_minute": 390}, + "at390 missing/clock") + require(isinstance(observation["operations"], dict) and + type(observation["domain_event_counter"]) is int and + type(observation["published_ids_count"]) is int and + observation["domain_event_counter"] >= observation["published_ids_count"] >= 0, + "at390 report/counter") + require(isinstance(observation["processed_arrival_minutes"], list) and + observation["processed_arrival_minutes"] == sorted(set(observation["processed_arrival_minutes"])) and + all(type(m) is int and m <= 390 for m in observation["processed_arrival_minutes"]), + "at390 processed minutes") + ids = observation["guest_auto_ids"] + require(isinstance(ids, list) and ids == sorted(set(ids)) and + type(observation["guest_auto_count"]) is int and + len(ids) == observation["guest_auto_count"] and + all(x.startswith("guest-auto-") for x in ids), "at390 guests") + require(all(type(e["absolute_minute"]) is int and + report["saved_minute"] < e["absolute_minute"] <= 390 + for e in observation["events"]), "at390 event window") + event_ids = [e["event_id"] for e in observation["events"]] + require(len(event_ids) == len(set(event_ids)), "at390 duplicate event") + at_rows = [r for r in rows if r["after"] == 390] + require(len(at_rows) == 1 and at_rows[0]["operations_observation"] == + {k: v for k, v in observation.items() if k != "events"}, "at390 minute consistency") + require(all(e in at_rows[0]["recent_ids"] for e in event_ids), "at390 event window IDs") boundary = report["boundary"] require(boundary is not None and boundary["guest_auto_count"] > 0, "arrival not processed") require(len(boundary["guest_auto_ids"]) == len(set(boundary["guest_auto_ids"])) == @@ -292,17 +508,14 @@ def check_report(report): require([r["minute"] for r in next_boundary["arrival_reports"]] == minutes, "arrival report duplicates/omissions") else: - require(report["boundary"] is None and report["next_boundary"] is None, "R Operations boundary") + require(report["boundary"] is None and report["next_boundary"] is None and + report["at_390"] is None, "R Operations boundary") return True def check_no_metrics(value): if isinstance(value, dict): for key, child in value.items(): - # Mandatory verbatim domain/IO payloads contain product diagnostics. - # They are evidence, never performance measurements or verdicts. - if key in ("payload", "signal_payload", "latest"): - continue require(not key.endswith(("_us", "_ms")) and key not in ("performance", "statistics", "fps", "budget_verdict"), "performance field forbidden") check_no_metrics(child) @@ -311,9 +524,20 @@ def check_no_metrics(value): check_no_metrics(child) -def check_pair(writer, reader): - check_report(writer) - check_report(reader) +def canonical_observation(observation, eid): + result = copy.deepcopy(observation) + if result is not None: + result["events"] = [ + {"event_id": eid, "replaced_for_cross_pid_compare": True, + "canonical_sha256": e["canonical_sha256"]} if e["event_id"] == eid else e + for e in result["events"]] + return result + + +def check_pair(writer, reader, writer_sidecar=None, reader_sidecar=None): + check_report(writer, writer_sidecar) + check_report(reader, reader_sidecar) + writer, reader = writer_sidecar["report"], reader_sidecar["report"] require(writer["role"].replace("writer", "reader") == reader["role"] and writer["pid"] != reader["pid"], "pair roles/PID") require(writer["event_id"] == reader["event_id"] and writer["saved_minute"] == reader["saved_minute"], @@ -326,6 +550,8 @@ def check_pair(writer, reader): "cross-PID complete typed state mismatch") require(writer["boundary"] == reader["boundary"] and writer["next_boundary"] == reader["next_boundary"], "Operations boundary differs") + require(canonical_observation(writer["at_390"], writer["event_id"]) == + canonical_observation(reader["at_390"], reader["event_id"]), "at390 pair differs") def bound_raw(folder, row): @@ -385,10 +611,21 @@ def bound_raw(folder, row): close.get("deadline_reached") is False and close.get("clock_process_held_stopped") is True and close.get("window_input_held_disabled") is True and close.get("restored_before_quit") is False and close.get("shared_bus_deleted") is False and close.get("settings_mutated") is False, "audio close") - check_report(report) + check_report(report, bound_sidecar(folder.parent, row)) return report +def bound_sidecar(out, row): + binding = row["sidecar"] + require(set(binding) == {"path", "sha256", "bytes"} and binding["path"] == SIDECAR, + "sidecar binding/path") + expected = {k: binding[k] for k in ("sha256", "bytes")} + path = Path(out) / (row["phase"] + "-states") / "report_full.json" + require(pin(path) == expected and pin(Path(row["runtime_profile"]) / SIDECAR) == expected, + "sidecar SHA/private profile binding") + return json.loads(path.read_text()) + + def check_receipt(receipt, out): from restart_pair import COLLECTOR_PINS, check_transfer out = Path(out) @@ -432,12 +669,15 @@ def check_receipt(receipt, out): for name, evidence in report["states"].items(): require(pin(out / (row["phase"] + "-states") / (name + ".bin")) == {k: evidence[k] for k in ("sha256", "bytes")}, "state bytes SHA") + require(receipt["profiles"][row["runtime_profile"]].get(SIDECAR) == + {k: row["sidecar"][k] for k in ("sha256", "bytes")}, "sidecar absent from file map") require(len(set(pids)) == len(pids), "reused PID") require({r["pid"] for r in receipt["cleanup"]} == set(pids) and all( r["engine_ended"] is True and r["outer_cleanup_errors"] == [] and r["foreign_process_or_group_signals"] == 0 for r in receipt["cleanup"]), "cleanup mismatch") for start in (3, 5): - check_pair(rows[start]["report"], rows[start + 1]["report"]) + check_pair(rows[start]["report"], rows[start + 1]["report"], + bound_sidecar(out, rows[start]), bound_sidecar(out, rows[start + 1])) transfer = out / (rows[start]["phase"][0] + "-transfer") manifest_path = transfer / "manifest.json" manifest = json.loads(manifest_path.read_text()) @@ -457,6 +697,8 @@ def check_receipt(receipt, out): require(sha(out / "restart_probe.gd") == receipt["worker_before"]["restart_probe.gd"], "executed probe differs from submitted source") require(receipt["owned_writes"] == [], "probe/driver wrote owned folder") + require(receipt["coverage"] == coverage([bound_sidecar(out, r)["report"] for r in rows[3:]]), + "overload coverage/persistence claim") return True diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py index b565c03..2ec4efd 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_pair.py @@ -149,6 +149,7 @@ def bind_report(out, label, row): reports = [s.partition("=")[2] for s in text.splitlines() if s.startswith("P28_RESTART_REPORT=")] check.require(len(reports) == 1, "one raw report required") row["report"] = json.loads(reports[0]) + copy_states(out, row) check.bound_raw(out / label, row) return row["report"] @@ -157,6 +158,10 @@ def copy_states(out, row): folder = out / (row["phase"] + "-states") folder.mkdir() profile = Path(row["runtime_profile"]) + source = profile / check.SIDECAR + row["sidecar"] = {"path": check.SIDECAR, **check.pin(source)} + shutil.copyfile(source, folder / "report_full.json") + check.require(check.pin(source) == check.pin(folder / "report_full.json"), "sidecar copy differs") for name, expected in row["report"]["states"].items(): source = profile / expected["path"] check.require(source.resolve().is_relative_to(profile.resolve()) and @@ -267,14 +272,14 @@ def main(): row.update(runtime_profile=str(runtime), runtime_name=runtime_name, guard=observation, guard_sha256=check.sha(guard_path)) report = bind_report(out, label, row) - copy_states(out, row) collector.write(out / (label + "-report.json"), report) if label.endswith("writer"): create_transfer(runtime / "p28_restart/good", transfer, collector.write) else: check.require(report["transfer_manifest_sha256"] == check.sha(transfer / "manifest.json"), "reader transfer manifest binding") - check.check_pair(rows[-2]["report"], report) + check.check_pair(rows[-2]["report"], report, check.bound_sidecar(out, rows[-2]), + check.bound_sidecar(out, row)) else: check.bound_raw(out / label, row) check.require(row["source_preserved"] and row["player_preserved"] and @@ -297,6 +302,13 @@ def main(): "profiles": {str(p): collector.profile_files(p) for p in profiles}, "owned_writes": [], "cross_cutting": check.CROSS, "original_credit": 0, "windows_verified": False, "operations_200_guests_verified": False} + try: + receipt["coverage"] = check.coverage([ + check.bound_sidecar(out, row)["report"] for row in rows if "sidecar" in row]) + except (ValueError, KeyError, TypeError, OSError) as caught: + receipt["coverage"] = {"overload_nonzero_backlog": "not_exercised", + "error": repr(caught)} + receipt.update(passed=False, error=receipt["error"] or "coverage: " + repr(caught)) if receipt["passed"]: try: check.check_receipt(receipt, out) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd index 1d62300..aaeecc8 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -16,6 +16,65 @@ var E := "" var package: Dictionary = {} var failure_event: Dictionary = {} var boundary: Variant = null +var at_390_event_verbatim: Array = [] +var at_390_pending: Array = [] +var redaction_ok := true +const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SAVE_LOADED", + "SAVE_LOAD_FAILED", "SAVE_MIGRATION_SUCCEEDED", "SAVE_MIGRATION_FAILED", + "SAVE_RECOVERY_LOADED", "SAVE_RECOVERY_CANDIDATE_LOADED", "SAVE_RECOVERY_FAILED"] + +func canonical_json(value: Variant) -> String: + if value is Dictionary: + var keys: Array = value.keys() + keys.sort() + var parts: PackedStringArray = [] + for key: Variant in keys: + parts.append(JSON.stringify(str(key)) + ":" + canonical_json(value[key])) + return "{" + ",".join(parts) + "}" + if value is Array: + var parts: PackedStringArray = [] + for child: Variant in value: + parts.append(canonical_json(child)) + return "[" + ",".join(parts) + "]" + if value is int or value is float: + return "%.17f" % float(value) + return JSON.stringify(value) + +func event_summary(event: Dictionary) -> Dictionary: + var canonical: Dictionary = {"event_id": E, "replaced_for_cross_pid_compare": true} \ + if field(event, "event_id") == E else event + return {"event_id": field(event, "event_id"), "event_type": field(event, "event_type"), + "absolute_minute": field(event, "absolute_minute"), + "raw_event_sha256": bytes_hash(canonical_json(event).to_utf8_buffer()), + "canonical_sha256": bytes_hash(canonical_json(canonical).to_utf8_buffer())} + +func observe_390() -> void: + if not role.begins_with("O-") or GameClock.absolute_minute != 390 or report.at_390 != null: + return + var observed := operations_observation() + var state := witness(runtime.create_snapshot()) + report.at_390 = {"clock": {"absolute_minute": GameClock.absolute_minute}, + "operations": observed.operations, "processed_arrival_minutes": observed.processed_arrival_minutes, + "guest_auto_ids": observed.guest_auto_ids, "guest_auto_count": observed.guest_auto_count, + "published_ids_count": state.published_ids.size(), "domain_event_counter": state.counter, + "events": []} + at_390_pending = state.recent_events.duplicate(true) + if report.has("saved_minute"): + bind_390_events() + +func bind_390_events() -> void: + if report.at_390 == null: + return + at_390_event_verbatim = [] + report.at_390.events = [] + for event: Dictionary in at_390_pending: + var minute: int = field(event, "absolute_minute") + if minute > report.saved_minute and minute <= 390: + at_390_event_verbatim.append(event.duplicate(true)) + report.at_390.events.append(event_summary(event)) + +func deadline_timer_seconds() -> float: + return Time.get_ticks_msec() / 1000.0 func field(d: Variant, name: String) -> Variant: if d is Dictionary: @@ -117,14 +176,14 @@ func operations_observation() -> Dictionary: "arrival_reports": arrivals} func on_tick(minute: int) -> void: + # During deterministic stepping the observation is made after advance returns. + if not io_ready: + observe_390() if io_ready or finished or selected_tick: return var observed := operations_observation() if role == "O-writer": report.ui_ticks.append(observed) - if minute == 390: - report["at_390"] = observed.duplicate(true) - report.at_390["domain_events"] = field(runtime_part(runtime.create_snapshot()), "domain_events") var stop: bool = (role == "R-writer" and minute - start_minute >= 10) or \ (role == "O-writer" and (observed.guest_auto_count > 0 or minute - start_minute >= 120)) if stop: @@ -147,10 +206,18 @@ func begin() -> void: report = {"schema": "p28-restart-v1", "role": role, "pid": OS.get_process_id(), "main_added": false, "k_target": 30, "actual_k": 0, "io": [], "states": {}, "minutes": [], "ui_ticks": [], "overloads": [], "boundary": null, "next_boundary": null, + "at_390": null, "comparison_scope": "complete_typed_var_to_bytes", "excluded_paths": [], "transfer_before_guard_writes": 0, "transfer_verified": false, "probe_deadline_seconds": 1740} - create_timer(1740.0).timeout.connect(func() -> void: finish("probe_deadline")) + var elapsed := deadline_timer_seconds() + var interval := 1740.0 - elapsed + report["probe_deadline"] = {"limit_seconds_from_engine_start": 1740, + "engine_elapsed_at_begin_seconds": elapsed, "timer_seconds": interval} + if interval <= 0: + finish("probe_deadline") + return + create_timer(interval).timeout.connect(func() -> void: finish("probe_deadline")) if OS.get_environment("THE_TREE_RESTART_K") != "30" or role not in ["R-writer", "R-reader", "O-writer", "O-reader"]: finish("invalid_environment") return @@ -217,6 +284,7 @@ func begin() -> void: return report["event_id"] = E report["saved_minute"] = GameClock.absolute_minute + bind_390_events() boundary = operations_observation() if role.begins_with("O-") else null report["boundary"] = boundary var idle_before: Dictionary = runtime.create_snapshot() @@ -245,6 +313,9 @@ func begin() -> void: return report["final_minute"] = GameClock.absolute_minute if role.begins_with("O-"): + if report.at_390 == null: + finish("at_390_not_reached") + return var last := operations_observation() var added: Array = [] for minute: Variant in last.processed_arrival_minutes: @@ -328,6 +399,9 @@ func io_proof(base: Dictionary, after: Dictionary, kind: String, returned: Varia func writer_save() -> bool: var before: Dictionary = runtime.create_snapshot() + var backlog := GameClock.get_backlog_minutes() + report["overload_case"] = {"exercised": not report.overloads.is_empty() and backlog > 0, + "overloads": report.overloads.size(), "backlog_at_save": backlog} signals = [] var returned: bool = main.call("_save_manual_at_path", "user://p28_restart/good", "P28 RESTART") var after: Dictionary = runtime.create_snapshot() @@ -409,6 +483,7 @@ func deterministic_step() -> bool: var after: int = GameClock.absolute_minute if after != before + 1: return false + observe_390() var state: Dictionary = runtime.create_snapshot() var normalized := state.duplicate(true) var recent: Array = field(field(runtime_part(normalized), "domain_events"), "recent_events") @@ -429,15 +504,86 @@ func deterministic_step() -> bool: "excluded_paths": [], "replacement": replacement, "replacement_count": count, "recent_count": recent.size(), "recent_ids": recent_ids}) report.actual_k += after - before + if role.begins_with("O-") and after == 390: + var observation: Dictionary = report.at_390.duplicate(true) + observation.erase("events") + report.minutes[-1]["operations_observation"] = observation return GameClock.paused and backlog == GameClock.get_backlog_minutes() +func is_save_event(event: Variant) -> bool: + return event is Dictionary and field(event, "source_service") == "save_service" and \ + field(event, "event_type") in SAVE_EVENTS + +func redact_payload(payload: Dictionary, pointer: String) -> void: + if payload.has("timing_redactions"): + redaction_ok = false + return + var markers: Array = [] + for key: String in ["duration_us", "runtime_duration_us"]: + if not payload.has(key): + redaction_ok = false + return + markers.append({"original_key": key, "sidecar_ref": pointer + "/" + key}) + payload.erase(key) + payload["timing_redactions"] = markers + +func redact_event(event: Variant, pointer: String) -> void: + if is_save_event(event): + redact_payload(event.payload, pointer + "/payload") + +func public_report(full: Dictionary) -> Dictionary: + var result: Dictionary = full.duplicate(true) + for i in range(result.io.size()): + var row: Dictionary = result.io[i] + var pointer := "/report/io/%d" % i + for name: String in ["signal_payload", "latest"]: + if row.has(name): + redact_payload(row[name], pointer + "/" + name) + for name: String in ["event", "dropped"]: + if row.has(name): + redact_event(row[name], pointer + "/" + name) + for side: String in ["before", "after"]: + if row.has(side): + for j in range(row[side].recent_events.size()): + redact_event(row[side].recent_events[j], pointer + "/" + side + "/recent_events/%d" % j) + for k in range(result.minutes.size()): + redact_event(result.minutes[k].replacement, "/report/minutes/%d/replacement" % k) + return result + +func public_has_forbidden(value: Variant) -> bool: + if value is Dictionary: + for key: Variant in value: + if str(key).ends_with("_" + "us") or str(key).ends_with("_" + "ms") or \ + str(key) in ["performance", "statistics", "fps", "budget_verdict"]: + return true + if public_has_forbidden(value[key]): + return true + elif value is Array: + for child: Variant in value: + if public_has_forbidden(child): + return true + return false + func finish(reason: String) -> void: if finished: return finished = true report["stop_reason"] = reason report["key_forms"] = key_forms - print("P28_RESTART_REPORT=" + JSON.stringify(report)) + var sidecar_path := "user://p28_restart/evidence/report_full.json" + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(sidecar_path).get_base_dir()) + var file := FileAccess.open(sidecar_path, FileAccess.WRITE) + if file == null: + quit(1) + return + file.store_string(JSON.stringify({"report": report, "at_390_event_verbatim": at_390_event_verbatim}, "", true, true)) + file.close() + # Partial failure evidence stays in the sidecar. Never emit a leaking public payload. + var public: Dictionary = public_report(report) + if not redaction_ok or public_has_forbidden(public) or public.is_empty(): + print("P28_RESTART_EVIDENCE_FAILURE=" + reason) + else: + print("P28_RESTART_REPORT=" + JSON.stringify(public, "", true, true)) if is_instance_valid(main) and main.is_inside_tree(): main.ui_audio_window_close_drained.connect(emit_audio_close, CONNECT_ONE_SHOT) get_root().close_requested.emit() diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py index b551ad9..b388384 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -37,7 +37,8 @@ def io(kind, minute=612, count=256): payload = {"ok": kind != "tamper", "code": "OK" if kind != "tamper" else "CORRUPT_WORLD", "operation": "save" if kind == "save" else "load", "save_path": "user://p28_restart/" + ("tampered" if kind == "tamper" else "good"), - "save_id": "synthetic", "saved_absolute_minute": minute} + "save_id": "synthetic", "saved_absolute_minute": minute, + "duration_us": 124, "runtime_duration_us": 57} event = {"event_id": check.event_id(count), "event_type": {"save": "SAVE_CHECKPOINT_SUCCEEDED", "load": "SAVE_LOADED", "tamper": "SAVE_LOAD_FAILED"}[kind], @@ -57,20 +58,24 @@ def io(kind, minute=612, count=256): def report(role="R-writer", pid=100): writer, ops = role.endswith("writer"), role.startswith("O-") - minute = 391 if ops else 612 + minute = 361 if ops else 612 rows = [io("save", minute)] if writer else [io("tamper", 360, 3), io("load", minute)] names = ["P", "after_save", "final"] if writer else ["P", "B", "after_tamper", "after_load", "final"] event = rows[-1]["event"] boundary = {"clock": minute, "guest_auto_ids": ["guest-auto-00000001"], "guest_auto_count": 1, - "operations": {"enabled": True}, "processed_arrival_minutes": [390], - "arrival_reports": [{"minute": 390}]} if ops else None - next_boundary = {"minute": 420, "processed_once": True, "before": [390], "after": [390, 420], - "arrival_reports": [{"minute": 390}, {"minute": 420}], + "operations": {"enabled": True}, "processed_arrival_minutes": [360], + "arrival_reports": [{"minute": 360}]} if ops else None + next_boundary = {"minute": 390, "processed_once": True, "before": [360], "after": [360, 390], + "arrival_reports": [{"minute": 360}, {"minute": 390}], "guest_auto_ids": ["guest-auto-00000001", "guest-auto-00000002"]} if ops else None clock = {"minute": minute, "paused": True, "backlog": 121, "accumulator_hex": "00"} - return {"schema": "p28-restart-v1", "role": role, "pid": pid, "stop_reason": "complete", + result = {"schema": "p28-restart-v1", "role": role, "pid": pid, "stop_reason": "complete", "main_added": True, "k_target": 30, "actual_k": 30, "comparison_scope": "complete_typed_var_to_bytes", "excluded_paths": [], "transfer_before_guard_writes": 0, "probe_deadline_seconds": 1740, + "probe_deadline": {"limit_seconds_from_engine_start": 1740, + "engine_elapsed_at_begin_seconds": 12.25, "timer_seconds": 1727.75}, + "overloads": [], + "overload_case": {"exercised": False, "overloads": 0, "backlog_at_save": 121}, "io": rows, "package_matches_before": comparison(), "failure_event_absent": True, "transfer_verified": True, "event_id": event["event_id"], "saved_minute": minute, "start_minute": 360 if ops else 602, "final_minute": minute + 30, @@ -85,7 +90,37 @@ def report(role="R-writer", pid=100): "replacement_count": 1 if n < 5 else 0, "recent_count": 256, "recent_ids": history(255)["published_ids"] + ([event["event_id"]] if n < 5 else ["domain_event.000000000256"])} - for n in range(30)]} + for n in range(30)], "at_390": None} + if ops: + result["at_390"] = {"clock": {"absolute_minute": 390}, "operations": {"enabled": True}, + "processed_arrival_minutes": [360], + "guest_auto_ids": ["guest-auto-00000001"], "guest_auto_count": 1, + "domain_event_counter": 300, "published_ids_count": 300, "events": []} + result["minutes"][28]["operations_observation"] = { + k: copy.deepcopy(v) for k, v in result["at_390"].items() if k != "events"} + return result + + +def evidence(full, events=None): + sidecar = {"report": copy.deepcopy(full), "at_390_event_verbatim": copy.deepcopy(events or [])} + return check.redact_report(full), sidecar + + +def validate_report(full): + public, sidecar = evidence(full) + return check.check_report(public, sidecar) + + +def validate_pair(writer, reader): + public_writer, sw = evidence(writer) + public_reader, sr = evidence(reader) + return check.check_pair(public_writer, public_reader, sw, sr) + + +def summarize(event, eid): + return {**{k: event[k] for k in ("event_id", "event_type", "absolute_minute")}, + "raw_event_sha256": check.json_sha(event), + "canonical_sha256": check.json_sha(check.canonical_event(event, eid))} def write(path, value): @@ -143,8 +178,9 @@ def synthetic_receipt(out): observed = report(label, pid) if label.endswith("reader"): observed["transfer_manifest_sha256"] = check.sha(out / (label[0] + "-transfer/manifest.json")) - row["report"] = observed - raw = "P28_RESTART_REPORT=" + json.dumps(observed) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(pid)) + "\n" + public, sidecar = evidence(observed) + row["report"] = public + raw = "P28_RESTART_REPORT=" + json.dumps(public) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(pid)) + "\n" profile = str(out / (label + "-private")) row.update(runtime_profile=profile, runtime_name=label) profiles[profile] = {} @@ -161,6 +197,12 @@ def synthetic_receipt(out): row.update(guard=guard, guard_sha256=check.sha(guard_path)) states = out / (label + "-states") states.mkdir() + sidecar_path = Path(profile) / check.SIDECAR + sidecar_path.parent.mkdir(parents=True) + write(sidecar_path, sidecar) + (states / "report_full.json").write_bytes(sidecar_path.read_bytes()) + row["sidecar"] = {"path": check.SIDECAR, **check.pin(sidecar_path)} + profiles[profile][check.SIDECAR] = check.pin(sidecar_path) for name in observed["states"]: (states / (name + ".bin")).write_bytes(DATA) for name, content in (("stdout.log", raw), ("stderr.log", ""), ("engine-native.log", "native\n")): @@ -185,6 +227,7 @@ def synthetic_receipt(out): "worker_before": worker, "worker_after": worker.copy(), "profiles": profiles, "cross_cutting": copy.deepcopy(check.CROSS), "original_credit": 0, "windows_verified": False, "operations_200_guests_verified": False, + "coverage": check.coverage([report("R-writer"), report("O-writer")]), "script_pins": {str(probe): check.sha(probe)}, "owned_writes": [], "cleanup": [{"pid": 100 + n, "engine_ended": True, "outer_cleanup_errors": [], "foreign_process_or_group_signals": 0} for n in range(7)]} @@ -269,11 +312,11 @@ class RestartTests(unittest.TestCase): "failed event retained": lambda x: x.update(failure_event_absent=False), "backlog consumed": lambda x: x["paused_idle"]["after"].update(backlog=120), "reader not paused": lambda x: x["io"][1].update(paused_after=False), - }, check.check_report) + }, validate_report) def test_rs3_actual_k_full_typed_cross_pid(self): writer, reader = report(), report("R-reader", 101) - check.check_pair(writer, reader) + validate_pair(writer, reader) self.rejects(reader, { "K call count": lambda x: x["minutes"][0].update(after=x["saved_minute"]), "different K": lambda x: x.update(actual_k=29), @@ -285,11 +328,11 @@ class RestartTests(unittest.TestCase): "after window": lambda x: x["minutes"][6].update(replacement=x["io"][-1]["event"], replacement_count=1), "excluded path": lambda x: x["minutes"][0].update(excluded_paths=["clock"]), "same PID": lambda x: x.update(pid=100), - }, lambda x: check.check_pair(writer, x)) + }, lambda x: validate_pair(writer, x)) def test_rs4_rs5_arrival_boundary(self): writer, reader = report("O-writer"), report("O-reader", 101) - check.check_pair(writer, reader) + validate_pair(writer, reader) self.rejects(writer, { "boundary only": lambda x: x["boundary"].update(guest_auto_count=0), "null arrival": lambda x: x.update(boundary=None), @@ -299,7 +342,7 @@ class RestartTests(unittest.TestCase): "next missing": lambda x: x["next_boundary"].update(processed_once=False), "next duplicate": lambda x: x["next_boundary"]["after"].append(420), "cap exceeded": lambda x: x.update(start_minute=200), - }, check.check_report) + }, validate_report) def test_d1_import_closure(self): check.check_dependencies() @@ -479,6 +522,267 @@ class RestartTests(unittest.TestCase): _, errors = outer.finish_captured(outer.OWNED) self.assertEqual(errors, []) + def test_v1_at390_required_and_pair_consistency(self): + writer, reader = report("O-writer"), report("O-reader", 101) + validate_pair(writer, reader) + self.rejects(writer, { + "absent": lambda x: x.pop("at_390"), + "null": lambda x: x.update(at_390=None), + "not reached": lambda x: x.update(stop_reason="at_390_not_reached"), + "clock": lambda x: x["at_390"]["clock"].update(absolute_minute=391), + "counter type": lambda x: x["at_390"].update(domain_event_counter="300"), + "row differs": lambda x: x["minutes"][28]["operations_observation"].update(guest_auto_count=2), + }, validate_report) + for mutate in (lambda x: x.pop("at_390"), lambda x: x.update(at_390=None)): + w, r = copy.deepcopy(writer), copy.deepcopy(reader) + mutate(w) + mutate(r) + with self.assertRaises((ValueError, KeyError)): + validate_pair(w, r) + different = copy.deepcopy(reader) + different["at_390"]["operations"]["population"] = 3 + different["minutes"][28]["operations_observation"]["operations"]["population"] = 3 + validate_report(different) + with self.assertRaises(ValueError): + validate_pair(writer, different) + with self.assertRaises((ValueError, KeyError)): + validate_pair(writer, {**reader, "at_390": None}) + + def test_v2_all_seven_families_restore_verbatim(self): + full = report() + # T4 and T6 are conditional: an earlier SAVE event is the dropped head. + old = io("save", minute=600, count=0)["event"] + row = full["io"][0] + row["before"]["recent_events"][0] = old + row["dropped"] = copy.deepcopy(old) + row["after"] = check.transform(row["before"], row["event"]) + public, sidecar = evidence(full) + check.check_report(public, sidecar) + paths = check.payload_paths(public) + for path in ("/report/io/0/signal_payload", "/report/io/0/latest", + "/report/io/0/event/payload", "/report/io/0/before/recent_events/0/payload", + "/report/io/0/after/recent_events/255/payload", "/report/io/0/dropped/payload", + "/report/minutes/0/replacement/payload"): + self.assertEqual(paths[path]["timing_redactions"], [ + {"original_key": key, "sidecar_ref": path + "/" + key} for key in check.TIMINGS]) + self.assertNotIn("duration_us", paths[path]) + self.assertEqual(public["io"][0]["after"]["recent_events"][0], + full["io"][0]["after"]["recent_events"][0]) + self.assertEqual(check.check_sidecar(public, sidecar), full) + for kind in ("save", "load", "tamper"): + full_row = io(kind) + check.check_io(full_row, kind) + self.rejects(sidecar, { + "root extra": lambda x: x.update(extra={}), + "performance use": lambda x: x["report"].update(statistics={"average": 1}), + "outside closure": lambda x: x["report"].update(elapsed_ms=1), + "nested timing": lambda x: x["report"]["io"][0]["event"]["payload"].update(nested={"latency_us": 1}), + "missing timing": lambda x: x["report"]["io"][0]["latest"].pop("duration_us"), + "tamper value": lambda x: x["report"]["io"][0]["latest"].update(code="CHANGED"), + "nonSAVE": lambda x: x["report"]["io"][0]["before"]["recent_events"][0].update(event_type="OLD"), + "wrong service": lambda x: x["report"]["io"][0]["before"]["recent_events"][0].update(source_service="other"), + }, lambda x: check.check_sidecar(public, x)) + self.rejects(public, { + "leaked key": lambda x: x["io"][0]["signal_payload"].update(duration_us=124), + "hidden payload leak": lambda x: x.update(payload={"hidden_ms": 1}), + "extra marker": lambda x: x["io"][0]["latest"]["timing_redactions"].append( + {"original_key": "other", "sidecar_ref": "/report/other"}), + "missing marker": lambda x: x["io"][0]["latest"].pop("timing_redactions"), + "one marker": lambda x: x["io"][0]["latest"]["timing_redactions"].pop(), + "wrong pointer": lambda x: x["io"][0]["latest"]["timing_redactions"][0].update( + sidecar_ref="/report/io/0/signal_payload/duration_us"), + "unresolved pointer": lambda x: x["io"][0]["latest"]["timing_redactions"][0].update( + sidecar_ref="/report/io/999/latest/duration_us"), + "extra marker field": lambda x: x["io"][0]["latest"]["timing_redactions"][0].update(extra=1), + "nonSAVE marker": lambda x: x["io"][0]["after"]["recent_events"][0]["payload"].update( + timing_redactions=copy.deepcopy(x["io"][0]["latest"]["timing_redactions"])), + "restore differs": lambda x: x.update(pid=999), + }, lambda x: check.check_sidecar(x, sidecar)) + with self.assertRaises(ValueError): + check.check_report(public) + # The raw equality check must see differences hidden by public redaction. + tampered = copy.deepcopy(sidecar) + tampered["report"]["io"][0]["latest"]["duration_us"] += 1 + self.assertEqual(check.redact_report(tampered["report"]), public) + with self.assertRaises(ValueError): + check.check_report(public, tampered) + + def test_v2_at390_full_array_binding(self): + full = report("O-writer") + events = [ + {"event_id": "domain_event.%012d" % n, "event_type": "ARRIVAL", + "absolute_minute": 380 + i, "source_service": "operations", "payload": {"count": i + 1}} + for i, n in enumerate((250, 251))] + full["at_390"]["events"] = [summarize(e, full["event_id"]) for e in events] + public, sidecar = evidence(full, events) + check.check_report(public, sidecar) + self.rejects(sidecar, { + "missing array": lambda x: x.pop("at_390_event_verbatim"), + "missing entry": lambda x: x["at_390_event_verbatim"].pop(), + "extra entry": lambda x: x["at_390_event_verbatim"].append(copy.deepcopy(events[0])), + "reordered": lambda x: x["at_390_event_verbatim"].reverse(), + "raw differs": lambda x: x["at_390_event_verbatim"][0]["payload"].update(count=7), + "nonSAVE timing": lambda x: x["at_390_event_verbatim"][0]["payload"].update(duration_us=1), + }, lambda x: check.check_sidecar(public, x)) + for field in ("raw_event_sha256", "canonical_sha256"): + changed = copy.deepcopy(full) + changed["at_390"]["events"][0][field] = "f" * 64 + pub, sc = evidence(changed, events) + with self.assertRaises(ValueError): + check.check_sidecar(pub, sc) + changed = copy.deepcopy(full) + changed["at_390"]["events"][0]["canonical_sha256"] = check.json_sha( + {"event_id": events[0]["event_id"], "replaced_for_cross_pid_compare": True}) + pub, sc = evidence(changed, events) + with self.assertRaises(ValueError): + check.check_sidecar(pub, sc) + # D5 operates on an entire E element, including type and timing differences. + # This exercises the binding independently of the strict > saved_minute filter. + canonical_observations = [] + raw_shas = [] + for kind in ("save", "load"): + raw_event = io(kind, 361)["event"] + raw_event["payload"]["duration_us"] += 1 if kind == "load" else 0 + value = copy.deepcopy(full) + value["at_390"]["events"] = [summarize(raw_event, value["event_id"])] + pub, sc = evidence(value, [raw_event]) + check.check_sidecar(pub, sc) + canonical_observations.append(check.canonical_observation(value["at_390"], value["event_id"])) + raw_shas.append(value["at_390"]["events"][0]["raw_event_sha256"]) + with self.assertRaises(ValueError): + check.check_report(pub, sc) # E at saved_minute is outside this observation's window. + self.assertNotEqual(raw_shas[0], raw_shas[1]) + self.assertEqual(*canonical_observations) + # All other event fields and canonical hashes remain significant cross PID. + reader = report("O-reader", 101) + reader["at_390"]["events"] = copy.deepcopy(full["at_390"]["events"]) + rp, rs = evidence(reader, events) + check.check_pair(public, rp, sidecar, rs) + changed_events = copy.deepcopy(events) + changed_events[0]["payload"]["count"] = 9 + reader["at_390"]["events"] = [summarize(e, reader["event_id"]) for e in changed_events] + rp, rs = evidence(reader, changed_events) + check.check_report(rp, rs) + with self.assertRaises(ValueError): + check.check_pair(public, rp, sidecar, rs) + + def test_v2_receipt_sidecar_binding_rejections(self): + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + check.check_receipt(receipt, out) + self.rejects(receipt, { + "unbound": lambda x: x["phases"][3].pop("sidecar"), + "wrong SHA": lambda x: x["phases"][3]["sidecar"].update(sha256="f" * 64), + "missing map": lambda x: x["profiles"][x["phases"][3]["runtime_profile"]].pop(check.SIDECAR), + "wrong path": lambda x: x["phases"][3]["sidecar"].update(path="other.json"), + "coverage omitted": lambda x: x.pop("coverage"), + "false coverage": lambda x: x["coverage"].update(overload_nonzero_backlog="exercised_recorded"), + "persistence claim": lambda x: x["coverage"].update(across_restart_backlog_persistence_claimed=True), + }, lambda x: check.check_receipt(x, out)) + path = out / "R-writer-states/report_full.json" + saved = path.read_bytes() + path.write_bytes(b"{}") + with self.assertRaises(ValueError): + check.check_receipt(receipt, out) + path.unlink() + with self.assertRaises(ValueError): + check.check_receipt(receipt, out) + path.write_bytes(saved) + private = Path(receipt["phases"][3]["runtime_profile"]) / check.SIDECAR + private.write_bytes(b"{}") + with self.assertRaises(ValueError): + check.check_receipt(receipt, out) + + def test_v3_overload_is_observation_not_persistence(self): + for count, backlog in ((0, 0), (0, 121), (1, 0), (1, 121)): + full = report() + full["overloads"] = [{"minute": 612, "backlog": backlog}] * count + full["paused_idle"]["before"]["backlog"] = backlog + full["paused_idle"]["after"]["backlog"] = backlog + full["overload_case"] = {"exercised": bool(count and backlog > 0), + "overloads": count, "backlog_at_save": backlog} + validate_report(full) + observed = check.coverage([full]) + self.assertEqual(observed["overload_nonzero_backlog"], + "exercised_recorded" if count and backlog else "not_exercised") + self.assertFalse(observed["across_restart_backlog_persistence_claimed"]) + self.rejects(full, { + "missing disclosure": lambda x: x.pop("overload_case"), + "wrong exercised": lambda x: x["overload_case"].update(exercised=not bool(count and backlog)), + "wrong count": lambda x: x["overload_case"].update(overloads=count + 1), + "wrong backlog": lambda x: x["overload_case"].update(backlog_at_save=backlog + 1), + "consumption": lambda x: x["paused_idle"]["after"].update(backlog=backlog + 1), + "persistence claim": lambda x: x.update(across_restart_backlog_persistence_claimed=True), + "nonzero claim": lambda x: x.update(nonzero_backlog_preserved=True), + }, validate_report) + writer, reader = report(), report("R-reader", 101) + reader["paused_idle"]["before"]["backlog"] = 0 + reader["paused_idle"]["after"]["backlog"] = 0 + for row in reader["minutes"]: + row.update(backlog_before=0, backlog_after=0) + validate_pair(writer, reader) # No across-restart accumulator persistence requirement. + + def test_v4_no_arrival_is_diagnostic_failure(self): + full = report("O-writer") + full.update(boundary=None, boundary_reason="no_arrival_within_cap", + stop_reason="no_arrival_within_cap", next_boundary=None, io=[], minutes=[], actual_k=0) + full["ui_ticks"] = [{"clock": n, "guest_auto_ids": [], "guest_auto_count": 0} + for n in range(361, 481)] + self.assertTrue(check.check_no_arrival_diagnostic(full)) + with self.assertRaisesRegex(ValueError, "incomplete probe"): + validate_report(full) + self.rejects(full, { + "missing reason": lambda x: x.pop("boundary_reason"), + "wrong reason": lambda x: x.update(boundary_reason="complete"), + "not null": lambda x: x.update(boundary={}), + "short cap": lambda x: x["ui_ticks"].pop(), + }, check.check_no_arrival_diagnostic) + with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: + out = Path(temporary) + receipt = synthetic_receipt(out) + row = receipt["phases"][5] + full["pid"] = row["report"]["pid"] + public, sidecar = evidence(full) + row["report"] = public + text = "P28_RESTART_REPORT=" + json.dumps(public) + "\nP28_RESTART_AUDIO_CLOSE=" + json.dumps(audio(full["pid"])) + "\n" + (out / "O-writer/stdout.log").write_text(text) + row["raw"]["stdout.log"] = check.pin(out / "O-writer/stdout.log") + row["process"]["stdout_sha256"] = row["raw"]["stdout.log"]["sha256"] + (out / "O-writer/process.json").write_text(json.dumps(row["process"])) + row["owned_files"]["process.json"] = check.pin(out / "O-writer/process.json") + for path in (out / "O-writer-states/report_full.json", + Path(row["runtime_profile"]) / check.SIDECAR): + path.write_text(json.dumps(sidecar)) + binding = check.pin(out / "O-writer-states/report_full.json") + row["sidecar"] = {"path": check.SIDECAR, **binding} + receipt["profiles"][row["runtime_profile"]][check.SIDECAR] = binding + with self.assertRaisesRegex(ValueError, "incomplete probe"): + check.check_receipt(receipt, out) + + def test_v5_engine_elapsed_deadline(self): + for elapsed in (0, .25, 37, 1739.5): + full = report() + full["probe_deadline"].update(engine_elapsed_at_begin_seconds=elapsed, timer_seconds=1740-elapsed) + validate_report(full) + value = report()["probe_deadline"] + self.rejects(value, { + "old relative timer": lambda x: x.update(timer_seconds=1740), + "wrong limit": lambda x: x.update(limit_seconds_from_engine_start=1800), + "negative elapsed": lambda x: x.update(engine_elapsed_at_begin_seconds=-1, timer_seconds=1741), + "missing elapsed": lambda x: x.pop("engine_elapsed_at_begin_seconds"), + "expired": lambda x: x.update(engine_elapsed_at_begin_seconds=1740, timer_seconds=0), + "nonfinite": lambda x: x.update(engine_elapsed_at_begin_seconds=float("inf")), + }, check.check_deadline) + check.check_deadline({**value, "engine_elapsed_at_begin_seconds": 1750, "timer_seconds": -10}, complete=False) + text = (check.HERE / "restart_probe.gd").read_text() + for altered in (text + "\nfunc forbidden():\n\tTime.get_ticks_msec()\n", + text.replace("deadline_timer_seconds() ->", "other_watchdog() ->"), + text.replace("GameClock.advance_minutes(1)", "Time.get_ticks_msec()"), + text + "\nfunc forbidden():\n\tTime.get_unix_time_from_system()\n"): + with self.assertRaises(ValueError): + check.check_probe(altered) + if __name__ == "__main__": unittest.main(verbosity=2) From e524ff2924a539535ea67f0135792b20f0f79729 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 03:09:04 +0900 Subject: [PATCH 4/5] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 17 +++++++++++++ .../p28-01/restart/probe/restart_probe.gd | 2 +- .../p28-01/restart/probe/test_restart.py | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py index 939d6d8..8dd6c3e 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -125,12 +125,29 @@ def check_modules(modules=None, root=ROOT): def check_probe(text): + # Focused guard for direct Variant inference only, not GDScript compilation. + # Root's actual Godot parse remains required. + lines = [raw.split("#", 1)[0] for raw in text.splitlines()] + variant_names = set() + variant_functions = {"field"} + for line in lines: + declaration = re.match(r"^var\s+(\w+)\s*:\s*Variant\b", line) + if declaration: + variant_names.add(declaration[1]) + declaration = re.match(r"^func\s+(\w+)\s*\(.*\)\s*->\s*Variant\b", line) + if declaration: + variant_functions.add(declaration[1]) function = None blocks = [] pauses = 0 watchdog_calls = 0 for raw in text.splitlines(): line = raw.split("#", 1)[0] + inferred = re.match(r"^\s*var\s+\w+\s*:=\s*(\w+)\b(.*)", line) + if inferred: + require(not (inferred[1] in variant_names or + (inferred[1] in variant_functions and inferred[2].lstrip().startswith("("))), + "direct Variant inference requires an explicit type") match = re.match(r"^func (\w+)\(", line) if match: function = match[1] diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd index aaeecc8..e9b88eb 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -399,7 +399,7 @@ func io_proof(base: Dictionary, after: Dictionary, kind: String, returned: Varia func writer_save() -> bool: var before: Dictionary = runtime.create_snapshot() - var backlog := GameClock.get_backlog_minutes() + var backlog: int = GameClock.get_backlog_minutes() report["overload_case"] = {"exercised": not report.overloads.is_empty() and backlog > 0, "overloads": report.overloads.size(), "backlog_at_save": backlog} signals = [] diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py index b388384..fdbbebc 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -426,6 +426,31 @@ class RestartTests(unittest.TestCase): with self.subTest(step=replacement), self.assertRaises(ValueError): check.check_probe(text.replace("GameClock.advance_minutes(1)", replacement)) + def test_y2_direct_variant_inference_guard(self): + text = (check.HERE / "restart_probe.gd").read_text() + check.check_probe(text) + snippets = [ + "var x := GameClock.get_backlog_minutes()", + 'var y := field(d,"k")', + "var x := runtime.create_snapshot()", + "var x := main", + "var x := other_variant.value", + "var x := other_field(d)", + ] + declarations = "\nvar other_variant: Variant\nfunc other_field(d) -> Variant:\n\treturn d\n" + for snippet in snippets: + with self.subTest(snippet=snippet), self.assertRaisesRegex(ValueError, "direct Variant inference"): + check.check_probe(text + declarations + "\nfunc bad():\n\t" + snippet + "\n") + for snippet in ( + "var x: int = GameClock.get_backlog_minutes()", + 'var y: Variant = field(d,"k")', + "var x := 1", + "var x := witness(d)", + "var x := GameClock_extra.get_backlog_minutes()", + ): + with self.subTest(allowed=snippet): + check.check_probe(text + "\nfunc allowed():\n\t" + snippet + "\n") + def test_rs7_rs8_full_receipt_rejections(self): with tempfile.TemporaryDirectory(dir=os.environ.get("BUILD_OUTPUT_DIR") or None) as temporary: out = Path(temporary) From 954528a7c5a0e27e2e50b642373168fa0a550aba Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Wed, 21 Oct 2026 03:50:39 +0900 Subject: [PATCH 5/5] =?UTF-8?q?P28-01-RESTART:=20=E7=8F=BEHEAD=E3=81=AE?= =?UTF-8?q?=E6=80=A7=E8=83=BD=E5=9F=BA=E6=BA=96=EF=BC=9A=E9=80=94=E4=B8=AD?= =?UTF-8?q?=E4=BF=9D=E5=AD=98=E3=83=BB=E5=88=A5PID=E5=86=8D=E9=96=8B?= =?UTF-8?q?=E3=81=AE=E5=86=8D=E7=8F=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p28-01/restart/probe/check_restart.py | 3 + .../p28-01/restart/probe/restart_probe.gd | 11 +++- .../p28-01/restart/probe/test_restart.py | 63 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py index 8dd6c3e..ce2fb42 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/check_restart.py @@ -314,6 +314,8 @@ def json_sha(value): # and Python. Fixed decimal form is valid JSON; keys and strings remain verbatim. def encode(v): if isinstance(v, dict): + require(len({str(k) for k in v}) == len(v), "canonical_key_collision") + require(all(type(k) is str for k in v), "canonical JSON requires text keys") return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + encode(v[k]) for k in sorted(v)) + "}" if isinstance(v, list): @@ -335,6 +337,7 @@ def check_sidecar(public, sidecar): "sidecar root/missing") check_no_metrics(public) full = sidecar["report"] + require(full.get("stop_reason") != "canonical_key_collision", "canonical_key_collision") require(public == redact_report(full), "public redaction differs from W2") restored = copy.deepcopy(public) for path, payload in payload_paths(restored).items(): diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd index e9b88eb..c864d33 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/restart_probe.gd @@ -19,6 +19,7 @@ var boundary: Variant = null var at_390_event_verbatim: Array = [] var at_390_pending: Array = [] var redaction_ok := true +var canonical_key_collision := false const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SAVE_LOADED", "SAVE_LOAD_FAILED", "SAVE_MIGRATION_SUCCEEDED", "SAVE_MIGRATION_FAILED", "SAVE_RECOVERY_LOADED", "SAVE_RECOVERY_CANDIDATE_LOADED", "SAVE_RECOVERY_FAILED"] @@ -26,7 +27,13 @@ const SAVE_EVENTS := ["SAVE_CHECKPOINT_SUCCEEDED", "SAVE_CHECKPOINT_FAILED", "SA func canonical_json(value: Variant) -> String: if value is Dictionary: var keys: Array = value.keys() - keys.sort() + keys.sort_custom(func(a, b): return str(a) < str(b)) + for index: int in range(1, keys.size()): + if str(keys[index - 1]) == str(keys[index]): + canonical_key_collision = true + # Unwind serialization before emitting failure evidence. + call_deferred("finish", "canonical_key_collision") + return "" var parts: PackedStringArray = [] for key: Variant in keys: parts.append(JSON.stringify(str(key)) + ":" + canonical_json(value[key])) @@ -567,6 +574,8 @@ func public_has_forbidden(value: Variant) -> bool: func finish(reason: String) -> void: if finished: return + if canonical_key_collision: + reason = "canonical_key_collision" finished = true report["stop_reason"] = reason report["key_forms"] = key_forms diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py index fdbbebc..b2b39d3 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py +++ b/docs/plans/original-completion-2026-10-02/execution/p28-01/restart/probe/test_restart.py @@ -252,6 +252,69 @@ class RestartTests(unittest.TestCase): self.assertIn("test_restart.py", self.inventory_before) self.assertEqual(self.here_inventory(), self.inventory_before) + def actual_canonical_sidecar(self): + relative = Path("native-e524ff2-canonical-failure-r01/report_full.json") + roots = [check.ROOT / "build", + check.ROOT.parent / ".runtime" / "P28-01-RESTART" / "build"] + if os.environ.get("BUILD_OUTPUT_DIR"): + roots.insert(0, Path(os.environ["BUILD_OUTPUT_DIR"])) + path = next((root / relative for root in roots if (root / relative).is_file()), None) + self.assertIsNotNone(path, "required retained native-e524ff2 sidecar is missing") + raw = path.read_bytes() + self.addCleanup(lambda: self.assertEqual(path.read_bytes(), raw)) + return json.loads(raw) + + def test_z1_actual_text_key_order_hash(self): + sidecar = self.actual_canonical_sidecar() + events = sidecar["at_390_event_verbatim"] + self.assertEqual(len(events), 16) + first = events[0] + self.assertEqual(first["event_id"], "domain_event.000000000009") + self.assertEqual(first["absolute_minute"], 362) + self.assertEqual(first["payload"], + {"old_state": 1, "reason": "ENTRY_COMPLETED", "state": 2}) + self.assertEqual(check.json_sha(first), + "9ca80bec0c605ecd288f7671952d30fe66c67f291c5a618ea54623f07394edb9") + for key in ("raw_event_sha256", "canonical_sha256"): + self.assertEqual(sidecar["report"]["at_390"]["events"][0][key], + "f28afa0899d7efea59ab99ae06276ba354779a73cac705fa4801162f27c592d7") + with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"): + check.check_sidecar(check.redact_report(sidecar["report"]), sidecar) + + def test_z1_all_actual_event_bindings(self): + original = self.actual_canonical_sidecar() + sidecar = copy.deepcopy(original) + full = sidecar["report"] + self.assertEqual(len(sidecar["at_390_event_verbatim"]), 16) + self.assertEqual(len(full["at_390"]["events"]), 16) + for event, summary in zip(sidecar["at_390_event_verbatim"], full["at_390"]["events"]): + summary["raw_event_sha256"] = check.json_sha(event) + summary["canonical_sha256"] = check.json_sha( + check.canonical_event(event, full["event_id"])) + self.assertEqual(sidecar["at_390_event_verbatim"], original["at_390_event_verbatim"]) + self.assertEqual(check.check_sidecar(check.redact_report(full), sidecar), full) + for index in range(16): + with self.subTest(index=index): + changed = copy.deepcopy(sidecar) + changed["report"]["at_390"]["events"][index]["raw_event_sha256"] = "0" * 64 + with self.assertRaisesRegex(ValueError, "at390 raw/canonical SHA"): + check.check_sidecar(check.redact_report(changed["report"]), changed) + + def test_z2_collision_rejected(self): + for ambiguous in ({1: "numeric", "1": "text"}, + {"nested": [{1: "numeric", "1": "text"}]}): + with self.subTest(value=ambiguous), self.assertRaisesRegex( + ValueError, "canonical_key_collision"): + check.json_sha(ambiguous) + full = report() + full["stop_reason"] = "canonical_key_collision" + public, sidecar = evidence(full) + with self.assertRaisesRegex(ValueError, "canonical_key_collision"): + check.check_report(public, sidecar) + public["stop_reason"] = "complete" + with self.assertRaises(ValueError): + check.check_report(public, sidecar) + def rejects(self, original, mutations, validator): for name, mutation in mutations.items(): with self.subTest(rejection=name):