diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/BLOCKED.md b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/BLOCKED.md new file mode 100644 index 0000000..76c18c7 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/BLOCKED.md @@ -0,0 +1,93 @@ +# 過去の blocked 記録(repair2 で解消済み) + +OPUS_STARTUP_LIFECYCLE_CLARIFICATION により、以下の pause の質問も解消しました。 +この文書は履歴です。現在の実装・検証状況は IMPLEMENTATION.md と UNIT_OUTPUT.txt を参照してください。 +以下の「未提出」「未実行」は、当時の状態を記録したものです。 + +## P28-02-DIAG: repair1 起動時の pause 呼出しの適用範囲 + +## repair1 の判断 + +OPUS_D2_SCOPE_CLARIFICATION により、以前の S/K/C の比較境界の質問は +解消しました。以下の以前の記録は履歴であり、現在の阻害理由ではありません。 + +残る確認は、起動準備で probe が `GameClock.set_paused(true)` を呼んでよいかです。 +補足の「collector の SPEC §4.3 と同じ順序」に参照される +`execution/p28-01/SPEC.md` の §4.3 手順2は、autoload 準備後、R の復元前に +この呼出しを要求しています。一方、P28-02 の SPEC §3.1 と TASK_INSTRUCTIONS は、 +live を変更できる API を fixture の restore と `advance_minutes(1)` のみに +限定しています。補足は main の追加を明示していますが、probe によるこの +pause 呼出しの例外は明示していません。 + +`scripts/core/simulation_kernel.gd:175-178` の `set_paused(true)` は +`paused` と `_pause_request_serial` を変更します。後者は今回明示された K 層の +対象です。したがって、読取 API とみなすことはできません。 +main の `_ready` による副作用は明示された main 追加に伴うものとして扱えますが、 +それとは別の probe の呼出しについて、許可を独自に広げません。 + +必要な確定は、collector §4.3 手順2の pause を起動準備の例外として許すのか、 +DIAG ではその手順を省略して restore → main 追加とするのか、の一点です。 +collector の windowed 実行や速度設定を DIAG に持ち込む要求ではありません。 + +確認した SPEC の strip SHA は +`0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6`、 +raw SHA は `f58378b6cb317478f9d26776d0a236804f4ac80789d93f1555133298e4328d24`。 +TASK_INSTRUCTIONS の raw SHA は +`b9b635b2598aee32efac662aa21d21fc88f55fbfdc6097cf07483e7fa5d32a7d` と一致しました。 + +実装 S は未提出、unit と native は未実行、PASS の主張はありません。 +git 書込み、他エージェント起動、Runner 操作、製品変更は行っていません。 + +## 以前の記録(S/K/C の質問は解消済み) + +対象 base: `8c06c3b44235a52f35898b1f6263e8ee064af256`。 +実装提出ではなく、仕様判断への差戻しです。受入の達成は主張しません。 + +## 必要な判断 + +SPEC §3.1・§3.3 と D2 の「live の全状態」は、 +`SimulationRuntime.create_snapshot()` が返す保存対象の全状態を指すのか、 +snapshot に含まれない内部状態も指すのかを確定してください。 + +前者なら、返された Dictionary 全体の `var_to_bytes` 完全一致を比較範囲として +明記する必要があります。後者なら、内部状態の対象と観測方法 +(Object 参照、循環参照、signal 接続、性能計測状態などの扱いを含む)が必要です。 +worker が独自に対象を除外して「全状態一致」と報告することはしません。 + +## base のソースで確認した根拠 + +- `scripts/core/simulation_kernel.gd:39-46` に性能計測状態、`_accumulator`、 + `_skip_next_process`、handler 登録、pause serial などが存在します。 + 同ファイル `clock_snapshot()`(209-215行)は `absolute_minute`、`speed`、 + `paused`、`snapshot_version` の4項目だけを返します。 +- `scripts/core/simulation_snapshot.gd:6-14` は、この `clock_snapshot()` と + GameState の snapshot を取り込みます。 +- `autoload/simulation_runtime.gd:521-585` の `create_snapshot()` は + 上記 envelope に各 system の保存用 snapshot を追加します。 + 同ファイル134-136行の `_mvp_manifest_cache_catalog`、 + `_mvp_manifest_cache_generation`、`_mvp_manifest_cache` は含まれません。 +- 同ファイル560行から呼ぶ `_create_mvp_content_manifest()` は、 + 11189-11195行で cache hit を判定し、miss 時にはこの内部キャッシュを書き換えます。 + したがって `create_snapshot()` の結果を前後で比較するだけでは、 + その呼出しによる内部キャッシュの変更を検出できません。 + +これは実際の測定区間で cache miss が発生したという報告ではありません。 +warm 30分によって cache が構築される可能性はあります。しかし、cache hit の場合も、 +snapshot の bytes 一致が証明する範囲と「全状態」の要求範囲を確定する必要があります。 +読み取り API としての `get_property_list()` / `get()` による独自走査を採る場合も、 +参照や実行時メタデータをどこまで含めるかは、この受入境界の判断になります。 + +## 確認済み事項と作業状態 + +- AGENTS.md、SPEC.md、TASK_INSTRUCTIONS.md と RESTART CLARIFICATION_02 を読取。 +- SPEC の `sha256(text.strip())` は契約値 + `0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6` と一致。 + raw-bytes SHA との差を阻害理由にはしていません。 +- 許可された外部 Python 依存6ファイルの SHA は CLARIFICATION_02 D1 とすべて一致。 +- tick ID による結合、inclusive 区間、observer 別記録の仕様に不足は見つかっていません。 +- scratch を構成できない項目は理由つき未測定にする規定があるため、 + その可否自体を今回の阻害理由にはしていません。 +- 変更はこの `diag/BLOCKED.md` のみ。driver・probe・checker・verify・unit 試験は未実装。 +- unit 試験、engine、正式 validation は未実行。PASS の主張はありません。 + 指定 tmp 配下の一時ディレクトリ作成・削除の確認のみ実施しました。 +- git 書込み、他エージェント起動、Runner 操作、製品変更は行っていません。 diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/IMPLEMENTATION.md b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/IMPLEMENTATION.md new file mode 100644 index 0000000..cf697f9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/IMPLEMENTATION.md @@ -0,0 +1,83 @@ +# P28-02-DIAG repair2 実装提出 + +対象: P28-02-D1/D2/D3/D4。変更範囲はこの diag ディレクトリのみ。 +製品、SPEC、TASK_INSTRUCTIONS、validation、予算、分母は変更していません。 +git 書込み、engine 起動、他エージェント、Runner 操作は行っていません。 +局所試験は engine 不使用の `python3 test_diag.py`。生出力は UNIT_OUTPUT.txt。 +native import/parse/3 PID、正式 validation、独立レビューは未実行で、Runner/Root の担当です。 + +適用した補足(ユーザー DATA の全文): + +- OPUS_D2_SCOPE_CLARIFICATION: `5775e7b512d9e90dbcfbcb181f5b64dc0fa8207e0947c0a4a6a5c9195565f9bd` +- OPUS_STARTUP_LIFECYCLE_CLARIFICATION: `967aebf27ff412f7a16ce771ef7d68abf8cfde0bb76ed19cf6dbf2817ca7acc6` +- SPEC strip SHA: `0420dd4d2a95db65adc685bca91b9ff686650af31675f193992d75909a676ac6` + +## 実装上の選択 + +- startup は pause → R の SHA と get_var(false) → restore → pause → main → 2 frame。 + main の前後、各 frame 後、B0/B1、復元直後を記録します。speed 操作や resume はありません。 +- warm と measurement は各30回の advance_minutes(1)、各回後に1 frame await。 + tick ID、13本の信号境界、query を全件残します。I0〜I11 は checker が差分で算出します。 + tick 完了後から次の tick 開始までの wall は独立欄です。warm は順位・統計から除きます。 +- wall は observer を含む inclusive 値です。callback 自身の時間を別記し、引きません。 + 自身の最終時刻取得後の timing record の追加・return は自己費用の計測外です。 +- 各区間について平均・nearest-rank P95・最大・N、query 件数・合計・統計を再計算します。 + 平均 wall の降順(同値時は区間番号)による3回の順位と一致結果を保持します。 + query が0件なら件数・合計は実測0、query の平均等は null と理由にします。 +- Runtime、GameState、Runtime が create_snapshot を呼ぶ32 service を各 N=5 で独立計測。 + service 一覧と現在の製品 source の一致も unit で検査します。合算はしません。 +- 各 sample 前後の S/K/C の bytes を probe 内で完全比較し、全層の SHA、K の値、C の状態、 + 差分 path を report に残します。S 不一致時のみ型・key・配列順を保持する正規形で差を説明し、 + dict 列挙順だけの差も失敗扱いです。診断では OPT の同等性緩和を適用しません。 +- C を S の観測前に読んで、観測自体の cache 書換えを隠さない構成です。 + cache miss は値を保持したまま明記し、公開 catalog 読取から manifest の一致も検査します。 + 前後 C 完全一致の条件を満たさなければ失敗です。返却 manifest の top-level と配列を + caller 側で変更して live が変わらないことも検査します。 +- scratch は snapshot の deep copy と tree に追加しない Kernel/GameState です。 + deep copy、SimulationSnapshot.validate、SimulationSnapshot.restore(envelope のみ)、 + SaveService.save_snapshot/load_snapshot を各 N=5。load 用保存は計測外です。 + 各操作後に scratch のコンテナを変更して S/K/C が保たれることを検査し、 + Kernel/GameState/budget の instance ID が解放後に無効であることを検査します。 + 保存先は guard 後の private user://p28_diag/scratch/ 配下です。 +- Runtime 全体の restore は initialize が必要で、列挙された scratch API 外のため理由つき未測定。 + Runtime 内部 validator と connector 再構築も、公開の独立観測境界がないため理由つき未測定。 + envelope の値を Runtime 全体の値、query の値を connector 全体の値とは表記しません。 +- SaveService は静的 API ですが、渡すデータと path は scratch だけです。静的な SaveService 自身の + 診断 budget は更新されます。これは補足の S/K/C 境界には含まれません。live Kernel の budget は + 全量比較の対象です。Runtime オブジェクトを scratch として起動したという主張はしません。 +- observer は main と2 frame の準備後に接続します。指定4 signal に加え、autosave の発生を + 記録する save_operation_completed にも受動接続し、終了前に全て元の接続一覧へ戻します。 +- PID 内の C 同一性には instance ID を含めます。PID 間では ID の数値を比べず、同一 catalog を + 指す関係・generation・manifest bytes を比較します。K の PID 間比較は補足に列挙された6項目 + (minute、paused、pause serial、skip、snapshot version、handler 集合)です。K の全 SHA と + budget の実測時間は PID 間の同一条件にしませんが、raw と PID 内の検査には全量残します。 +- 初期条件不一致は値を raw に保持して失敗し、管制判断へ戻します。実測を補正しません。 + +## Root の実行インターフェース(worker は実行しない) + +凍結した S を checkout した環境で、以下の CLI を使います。`--check` は読取と計画の表示だけです。 +`--run` は import → parse-base → parse-wrapper → diag-1 → diag-2 → diag-3 を順番に実行します。 + +``` +python3 diag_p28_02.py --check|--run --readonly --fixture --engine --output +``` + +driver は6ファイルの依存 SHA を確認して collector を読取専用 import します。 +RESTART の依存検査・owned raw 結合のコードを局所に適合して再利用していますが、 +RESTART の Python 自体は import しません。追加 package、ネットワークは使いません。 +全1407 source と提出した5コードファイルが frozen commit S と同じことを確認します。 +private profile、guard、1800秒 owned timeout、audio close drain、cleanup は collector と同じ経路です。 + +成功時、driver が execution.json と、stage 以外の全ファイルを SHA/size で列挙した bundle.json を +作ります。これは Root による実行結果です。失敗時は execution.json を保存し、成功 bundle を作りません。 +stage は Root に残し、各 phase の全 stage inventory を execution.json に記録します。 +全 raw、owned receipts、guard、startup project、probe/wrapper、入力 manifest は bundle に含みます。 +通常の profile ファイルも collector と同様に inventory を保存し、owned process cleanup と混同しません。 + +Root は出力一式(stage は省略可)を `$BUILD_OUTPUT_DIR/p28-02-diag-evidence` に置きます。 +`python3 verify_diag.py --project-root .` は bundle 欠落を拒否し、全 raw/SHA/PID/guard/cleanup と +現候補の1407 source・5コードの一致を結合します。S の実測として記録し、Q の実測とは呼びません。 +bundle 作成後にファイルを追加・編集すると inventory 不一致で拒否します。 + +原本加算は0、正式完了23/88、4/30、予算 false 6件を保持します。P27 完了は主張しません。 +repair3: 最終の局所 unit を再実行して PASS(19件、0.780秒)。 diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/UNIT_OUTPUT.txt b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/UNIT_OUTPUT.txt new file mode 100644 index 0000000..047a2a7 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/UNIT_OUTPUT.txt @@ -0,0 +1,25 @@ +test_bundle_missing_and_tampered (__main__.DiagnosticTests) ... ok +test_connections_and_preparation (__main__.DiagnosticTests) ... ok +test_cross_pid_only_named_kernel_fields (__main__.DiagnosticTests) ... ok +test_fixture_pause_and_int64_identity (__main__.DiagnosticTests) ... ok +test_headless_plan_and_stop_on_failure (__main__.DiagnosticTests) ... ok +test_import_closure_and_pins (__main__.DiagnosticTests) ... ok +test_missing_not_zero (__main__.DiagnosticTests) ... ok +test_nonadditive_scope (__main__.DiagnosticTests) ... ok +test_pid_local_catalog_identity (__main__.DiagnosticTests) ... ok +test_queries_and_observer_rejection (__main__.DiagnosticTests) ... ok +test_real_owned_python_child_and_raw_binding (__main__.DiagnosticTests) ... RUNTIME_SCALE_PROCESS_START {"name": "import", "pid": 62682} +ok +test_scratch_ownership (__main__.DiagnosticTests) ... ok +test_service_snapshot_inventory_matches_current_product (__main__.DiagnosticTests) ... ok +test_source_map_count_and_mutations (__main__.DiagnosticTests) ... ok +test_state_and_cache_mismatch (__main__.DiagnosticTests) ... ok +test_static_probe_and_product_mutations (__main__.DiagnosticTests) ... ok +test_three_pid_comparison (__main__.DiagnosticTests) ... ok +test_tick_id_and_minute_rejection (__main__.DiagnosticTests) ... ok +test_valid_raw_intervals (__main__.DiagnosticTests) ... ok + +---------------------------------------------------------------------- +Ran 19 tests in 0.780s + +OK diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/check_diag.py b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/check_diag.py new file mode 100644 index 0000000..6fa7684 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/check_diag.py @@ -0,0 +1,595 @@ +#!/usr/bin/env python3 +"""DIAG evidence validation; no engine launch.""" +import ast +import copy +import hashlib +import importlib.util +import json +import math +from pathlib import Path +import re +import sys +import sysconfig + +HERE = Path(__file__).resolve().parent +ROOT = HERE.parents[5] +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_NAMES = {"diag_p28_02.py", "check_diag.py", "verify_diag.py", "test_diag.py", "diag_probe.gd"} +LOCAL = {"docs/plans/original-completion-2026-10-02/execution/p28-02/diag/" + n for n in LOCAL_NAMES} +PHASES = ["import", "parse-base", "parse-wrapper", "diag-1", "diag-2", "diag-3"] +FIXTURE_SHA = "eb4f770071eb07cd7e136fe54c3f78768cc8de7a4264e62a9af1f06a8f90f9bd" +ENGINE_SHA = "c7cccbf8fb143e34e02fd6521e09be2c2b974f0d5db080b19071c9c570718ccf" +SEMANTICS = {"intervals": "inclusive_wall_including_observer", "observer_subtracted": False, + "breakdown": "independent_inclusive_not_additive", "path": "query_only_not_connector"} +SERVICES = ['weather', 'command_queue', 'id_generator', 'domain_events', 'world_service', + 'path_service', 'mana_network_service', 'mana_reserve', 'economy_service', 'facility_upkeep', + 'upkeep_settlement', 'inventory_service', 'kitchen_production', 'auto_purchase_plans', + 'elevators', 'legacy_stairs', 'logistics_dispatcher', 'facility_service', 'facility_cleanliness', + 'guest_service', 'staff_service', 'progression_service', 'relationship_service', + 'event_service', 'authority_service', 'defense_service', 'construction_service', + 'excavation_service', 'spatial_subslot_service', 'route_subslot_path_adapter', + 'facility_infrastructure_projector', 'peeping_room_service'] +SCRATCH = {'deep_copy', 'envelope_validate', 'envelope_restore', 'save', 'load'} +UNMEASURED = {'runtime_restore', 'runtime_validation_internal', 'connector_rebuild'} +K_FIELDS = {'absolute_minute', 'speed', 'paused', 'snapshot_version', '_skip_next_process', + '_pause_request_serial', '_abort_current_tick_requested', '_last_tick_advanced', + '_accumulator_f64_hex', 'handlers', '_sample_counts', '_totals', '_samples'} + + +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): + """Conservative finite call policy, plus mutation-site checks; native parse is separate.""" + code = '\n'.join(line.split('#', 1)[0] for line in text.splitlines()) + tokens = re.sub(r'"(?:\\.|[^"\\])*"', '""', code) + forbidden = (r'\b(?:set_script|set_process|set_physics_process|set_deferred|callv?|call_thread_safe|' + r'call_deferred_thread_group|emit_signal|subscribe|unsubscribe|initialize|reset|' + r'clear_performance_reports|register_tick_handler|unregister_tick_handler|' + r'process_real_delta|advance_one_minute|_set_speed|_toggle_pause)\s*\(') + require(not re.search(forbidden, tokens), 'forbidden product/hook call') + require(not re.search(r'\b(?:clock|runtime|main|target)\s*(?:\.[\w]+|\[[^\n]+\])+\s*(?:[+*/-]?=)(?!=)', code), + 'live property assignment') + require(not re.search(r'\b(?:set|set_indexed)\s*\(', code), 'dynamic mutation') + functions = dict(re.findall(r'^func (\w+)\([^\n]*\).*?:\n(.*?)(?=^func |\Z)', code, re.M | re.S)) + require(functions and len(functions) == len(re.findall(r'^func ', code, re.M)), 'probe functions') + for name, body in functions.items(): + if 'clock.set_paused(' in body: + require(name == 'begin' and body.count('clock.set_paused(true)') == 2 and + body.count('clock.set_paused(') == 2, 'pause only twice in preparation') + require(body.rfind('clock.set_paused(') < body.index('main = load('), 'pause after main') + if re.search(r'\badvance_\w+\(', body): + require(name == 'step_window' and body.count('clock.advance_minutes(1)') == 1 and + len(re.findall(r'\badvance_\w+\(', body)) == 1, 'manual stepping site') + if 'runtime.restore_snapshot(' in body: + require(name == 'begin' and body.count('runtime.restore_snapshot(snapshot)') == 1 and + body.index('runtime.restore_snapshot(') < body.index('main = load('), 'fixture restore only') + if 'SaveService.' in body or 'SimulationSnapshot.' in body: + require(name == 'scratch_samples', 'scratch APIs outside scratch') + if '.free(' in body: + require(name == 'scratch_samples' and body.count('.free(') == 2 and + 'scratch_clock.free()' in body and 'scratch_state.free()' in body, 'scratch free only') + require('set_paused(false)' not in code, 'resume forbidden') + for receiver, allowed in { + 'clock': {'get', 'set_paused', 'advance_minutes'}, + 'runtime': {'get', 'create_snapshot', 'restore_snapshot', 'get_save_policy'}, + 'main': {'get'}, 'target': {'create_snapshot'}, + 'SaveService': {'save_snapshot', 'load_snapshot'}, + 'SimulationSnapshot': {'validate', 'restore'}, + }.items(): + calls = re.findall(r'\b' + receiver + r'\.(\w+)\s*\(', code) + require(set(calls) <= allowed, 'unapproved call on ' + receiver) + # Private calls are forbidden; named get() reads above are the finite K/C boundary. + require(not re.search(r'\.\s*_\w+\s*\(', code), 'private product call') + require(code.count('root.add_child(main)') == 1 and code.count('root.close_requested.emit()') == 1, + 'main lifecycle') + require('get_var(false)' in code and 'await process_frame' in code, 'fixture/frame lifecycle') + require('scratch: Dictionary = before.snapshot.duplicate(true)' in code and + 'is_instance_id_valid(id)' in code and 'signal_inventory()' in code, 'ownership checks absent') + # Mutation receivers may not be aliased to bypass the site checks above. + for method, receiver, count in (('set_paused', 'clock', 2), ('advance_minutes', 'clock', 1), + ('restore_snapshot', 'runtime', 1), ('add_child', 'root', 1)): + require(len(re.findall(r'\.\s*' + method + r'\s*\(', tokens)) == count and + len(re.findall(r'\b' + receiver + r'\.' + method + r'\s*\(', tokens)) == count, + 'mutation receiver/site: ' + method) + require(len(re.findall(r'\bcall_deferred\s*\(', tokens)) == 1 and 'call_deferred("begin")' in code, + 'deferred product call') + methods = set(re.findall(r'\.\s*(\w+)\s*\(', tokens)) + require(methods <= { + 'new', 'start', 'update', 'finish', 'hex_encode', 'append', 'get', 'resize', 'encode_double', + 'sort', 'duplicate', 'get_instance_id', 'get_manifest_generation', 'create_snapshot', + 'keys', 'sort_custom', 'size', 'has', 'append_array', 'build_all_guest_definitions', + 'get_guest_class_ids', 'get_species_ids', 'get_staff_candidate_ids', 'get_unique_character_ids', + 'get_connections', 'get_object_id', 'get_method', 'get_name', 'stringify', 'is_empty', + 'get_ticks_usec', 'get_ticks_msec', 'advance_minutes', 'save_snapshot', 'validate', 'restore', + 'load_snapshot', 'free', 'merge', 'get_node', 'get_environment', 'get_process_id', 'parse_string', + 'set_paused', 'get_sha256', 'open', 'get_var', 'close', 'restore_snapshot', 'connect', + 'instantiate', 'add_child', 'get_save_policy', 'snapshot', 'disconnect', 'emit' + }, 'unapproved member call') + for path in re.findall(r'"(user://[^"\n]*)"', code): + require(path.startswith('user://p28_diag/scratch/'), 'nonprivate scratch path') + + +def integer(value, label): + require(type(value) is int and value >= 0, 'invalid nonnegative integer: ' + label) + return value + + +def hash_value(value): + require(isinstance(value, str) and re.fullmatch('[0-9a-f]{64}', value), 'invalid SHA256') + + +def stats(values): + require(values, 'missing samples cannot become zero') + for value in values: + integer(value, 'sample') + ordered = sorted(values) + return {'count': len(values), 'mean_us': sum(values) / len(values), + 'p95_us': ordered[math.ceil(len(values) * .95) - 1], 'max_us': ordered[-1]} + + +def check_layers(value, require_paused=True): + for layer in ('S', 'K', 'C'): + hash_value(value[layer]) + kernel, cache = value['kernel'], value['cache'] + require(set(kernel) == K_FIELDS, 'finite K boundary') + require(type(kernel['paused']) is bool and (not require_paused or kernel['paused'] is True) and + kernel['speed'] in (1, 2, 3), 'clock not paused') + require(re.fullmatch('[0-9a-f]{16}', kernel['_accumulator_f64_hex']), 'f64 accumulator') + require(set(kernel['handlers']) == {str(i) for i in range(11)}, 'handler phases') + for names in kernel['handlers'].values(): + require(names == sorted(set(names)), 'handler IDs') + for metric in ('_sample_counts', '_totals', '_samples'): + require(isinstance(kernel[metric], dict), 'budget state missing') + require(cache['filled'] is True and cache['generation'] == cache['current_generation'] and + cache['catalog_id'] == cache['current_catalog_id'], 'cache not filled') + require(isinstance(cache['catalog_id'], str) and re.fullmatch(r'-?[1-9][0-9]*', cache['catalog_id']), + 'catalog identity must preserve int64 as decimal text') + hash_value(cache['bytes_sha256']) + + +def check_comparison(row): + before, after = row['before'], row['after'] + check_layers(before) + check_layers(after) + require(row['equal'] == {'S': True, 'K': True, 'C': True}, 'S/K/C bytes differ') + require(before == after and row['differences'] == [], 'state evidence contradicts equality') + require(row['cache_miss_in_sample'] is False, 'cache changed in sample; return to governor') + require(row['cache_rebuilt_matches_catalog'] is None and row['cache_rebuild_check_reason'] == 'no_cache_miss', + 'cache miss evidence contradiction') + + +def check_connections(value): + require(set(value) == {'tick_started', 'tick_phase_started', 'tick_completed', + 'path_query_measured', 'save_operation_completed'}, 'signal boundary') + for row in value.values(): + require(row['count'] == len(row['targets']), 'connection count') + for target in row['targets']: + require(set(target) == {'object', 'method', 'flags'} and str(target['object']).isdigit() + and target['method'], 'connection target') + + +def summarize_ticks(ticks): + values = [[] for _ in range(12)] + paths = [[] for _ in range(12)] + frames = [] + for tick in ticks: + for index in range(12): + values[index].append(tick['edges'][index + 1]['at_us'] - tick['edges'][index]['at_us']) + for query in tick['queries']: + paths[query['interval']].append(query['duration_us']) + if tick['frame_wall_before_us'] is not None: + frames.append(tick['frame_wall_before_us']) + return {'intervals': {'I' + str(i): stats(values[i]) for i in range(12)}, + 'query_only': {'I' + str(i): {'count': len(paths[i]), 'total_us': sum(paths[i]), + 'stats': stats(paths[i]) if paths[i] else None, + 'reason': None if paths[i] else 'no_queries_observed'} for i in range(12)}, + 'inter_tick_frame_wall': stats(frames)} + + +def check_report(report): + require(report['schema'] == 'p28-02-diag-v1' and report['role'] in PHASES[3:], 'report schema/role') + require(report['stop_reason'] == 'complete' and report['errors'] == [], 'probe failed') + require(report['semantics'] == SEMANTICS, 'exclusive/additive/connector interpretation forbidden') + require(report['input_sha256'] == FIXTURE_SHA, 'fixture differs') + require(report['metadata']['engine_sha256'] == ENGINE_SHA and + report['metadata']['machine'] == 'Mac16,10' and report['metadata']['headless'] is True, + 'engine/machine/headless condition') + require(report['metadata']['measured_source'] == 'S' and report['metadata']['candidate_Q_measured'] is False, + 'S/Q attribution') + require(re.fullmatch(r'[0-9a-f]{40}', report['metadata']['commit']), 'S commit') + require(isinstance(report['seed'], str) and report['seed'].isdigit() and report['rng_state_typed_hex'], + 'seed/RNG state missing') + for value in report['rng_state_typed_hex'].values(): + require(isinstance(value, str) and re.fullmatch(r'(?:[0-9a-f]{2})+', value), 'typed RNG bytes') + for name in ('restored', 'B0', 'B1', 'outside_before'): + check_layers(report['baselines'][name], require_paused=name != 'restored') + for name, minute in (('B0', 602), ('B1', 632), ('outside_before', 662)): + require(report['baselines'][name]['kernel']['absolute_minute'] == minute, 'baseline minute') + require(report['startup']['ops'].get('code') == 'INVALID_START_TIME', 'main Ops result') + for name in ('before_main', 'after_main', 'frame1', 'frame2'): + check_layers(report['startup'][name]) + prep = report['preparation'] + require([row['operation'] for row in prep] == ['initial_pause', 'fixture_restore', 'restored_pause'], + 'preparation operations') + require(prep[1]['ok'] is True and prep[1]['state'] == report['baselines']['restored'], 'fixture restore result') + for row in (prep[0], prep[2]): + require(row['K_after']['paused'] is True and row['K_after']['_pause_request_serial'] == + row['K_before']['_pause_request_serial'] + 1, 'preparation pause serial') + ticks = report['ticks'] + require(len(ticks) == 60, 'warm/measurement counts') + previous_end = None + for index, tick in enumerate(ticks): + minute = 602 + index + require(tick['id'] == index + 1 and tick['start_minute'] == minute and tick['end_minute'] == minute + 1, + 'tick ID/minute continuity') + require(tick['region'] == ('warm' if index < 30 else 'measurement'), 'warm included in measurement') + edges = tick['edges'] + require(len(edges) == 13 and edges[0]['name'] == 'tick_started' and + edges[-1]['name'] == 'tick_completed', 'signal edges') + require(edges[0]['minute'] == minute and edges[-1]['minute'] == minute + 1, 'edge minute') + for phase, edge in enumerate(edges[1:-1]): + require(edge['name'] == 'phase' and edge['phase'] == phase and + edge['minute'] == minute + (phase != 0), 'phase wrongly joined by minute') + times = [integer(edge['at_us'], 'edge') for edge in edges] + require(times == sorted(times), 'nonmonotonic edges') + require(tick['frame_wall_before_us'] == (times[0] - previous_end if previous_end is not None else None), + 'inter-tick wall attribution') + if previous_end is not None: + integer(tick['frame_wall_before_us'], 'frame wall') + previous_end = times[-1] + for query in tick['queries']: + slot = integer(query['interval'], 'query interval') + require(slot < 12 and query['tick_id'] == tick['id'] and query['region'] == tick['region'], + 'query tick/interval binding') + integer(query['duration_us'], 'query duration') + require(times[slot] <= query['at_us'] <= times[slot + 1], 'query timestamp outside interval') + for query in report['queries_outside']: + require(query['tick_id'] is None and query['interval'] is None, 'outside query attribution') + integer(query['duration_us'], 'outside query') + observations = report['observer'] + for row in observations: + integer(row['duration_us'], 'observer cost') + require(row['callback'] in {'tick_started', 'tick_phase_started', 'tick_completed', + 'path_query_measured', 'save_operation_completed'}, 'observer callback') + for tick in ticks: + selected = [o for o in observations if o['tick_id'] == tick['id']] + counts = {name: sum(o['callback'] == name for o in selected) for name in + ('tick_started', 'tick_phase_started', 'tick_completed', 'path_query_measured')} + require(counts == {'tick_started': 1, 'tick_phase_started': 11, 'tick_completed': 1, + 'path_query_measured': len(tick['queries'])}, 'missing observer costs') + measurements = report['measurements'] + snapshots = {'runtime_snapshot', 'game_state_snapshot'} | {s + '_snapshot' for s in SERVICES} + require(set(measurements) == snapshots | SCRATCH | UNMEASURED, 'measurement inventory') + for name, measurement in measurements.items(): + if name in UNMEASURED: + require(measurement['samples'] is None and isinstance(measurement['reason'], str) and + measurement['reason'].strip() and measurement['scope'] == 'unmeasured', 'unmeasured is not zero') + continue + require(measurement['scope'] == 'independent_inclusive' and measurement['reason'] is None and + len(measurement['samples']) == 5, 'N=5 independent samples') + for sample in measurement['samples']: + integer(sample['duration_us'], 'outside duration') + check_comparison(sample) + require(sample['before']['kernel']['absolute_minute'] == 662, 'outside-tick time advanced') + if name in snapshots: + hash_value(sample['result_sha256']) + else: + require(sample['input'] == 'live_snapshot_deep_copy' and sample['ok'] is True and + sample['detached_mutation_checked'] is True, 'scratch ownership') + require(sample['scratch_path'].startswith('user://p28_diag/scratch/') and + '..' not in sample['scratch_path'].split('/'), 'scratch private path') + require(len(sample['scratch_ids']) == len(sample['released']) == 3 and + all(value is True for value in sample['released']), 'scratch not released') + require(len(set(sample['scratch_ids'])) == 3, 'scratch IDs') + require(all(isinstance(v, str) and re.fullmatch(r'-?[1-9][0-9]*', v) + for v in sample['scratch_ids']), 'scratch int64 IDs') + check_comparison(report['outside_total']) + check_comparison(report['manifest_detached']) + for name in ('observer_before', 'observer_connected', 'observer_disconnected'): + check_connections(report[name]) + require(report['observer_before'] == report['observer_disconnected'] and + report['all_connections_restored'] is True, 'observer leak') + for name in report['observer_before']: + expected = report['observer_before'][name]['count'] + 1 + require(report['observer_connected'][name]['count'] == expected, 'observer connection not made') + require([v['label'] for v in report['cache_preparations']] == ['restored', 'B1', 'outside_tick'], + 'cache preparation records') + for row in report['cache_preparations']: + require(row['after']['filled'] is True and row['filled_outside_sample'] is (not row['before']['filled']), + 'cache preparation mismatch') + return summarize_ticks(ticks[30:]) + + +def preparation_key(report): + """Object IDs are PID-local identities; compare catalog relationship and contents across PIDs.""" + def stable(value): + result = copy.deepcopy(value) + if isinstance(result, dict): + if set(result) == K_FIELDS: + # Lifecycle clarification names this finite cross-PID K subset. + # The full K bytes (including nondeterministic path-query wall + # budgets) remain mandatory for within-PID outside-tick checks. + return {k: result[k] for k in ('absolute_minute', 'paused', '_pause_request_serial', + '_skip_next_process', 'snapshot_version', 'handlers')} + # C's SHA contains the required local object identity. Compare its underlying + # stable generation/content fields across PID, preserving raw C SHA in report. + result.pop('C', None) + result.pop('K', None) + for key in ('catalog_id', 'current_catalog_id'): + result.pop(key, None) + return {k: stable(v) for k, v in result.items()} + if isinstance(result, list): + return [stable(v) for v in result] + return result + return stable({'input': report['input_sha256'], 'seed': report['seed'], + 'rng': report['rng_state_typed_hex'], 'preparation': report['preparation'], + 'baselines': {k: report['baselines'][k] for k in ('restored', 'B0', 'B1')}, + 'startup': report['startup'], 'cache_preparations': report['cache_preparations']}) + + +def compare_runs(reports): + require(len(reports) == 3 and len({r['pid'] for r in reports}) == 3, 'three separate PIDs required') + summaries = [check_report(r) for r in reports] + keys = [preparation_key(r) for r in reports] + require(keys[0] == keys[1] == keys[2], 'preparation mismatch; raw values retained, return to governor') + ranks = [sorted(s['intervals'], key=lambda k: (-s['intervals'][k]['mean_us'], int(k[1:]))) for s in summaries] + return {'runs': summaries, 'mean_wall_rankings': ranks, 'rankings_agree': ranks[0] == ranks[1] == ranks[2], + 'rank_method': 'descending mean inclusive wall; ties by interval index', + 'outside': [{name: stats([v['duration_us'] for v in m['samples']]) if m['samples'] is not None + else {'value': None, 'reason': m['reason']} for name, m in r['measurements'].items()} + for r in reports], + 'observer': [stats([o['duration_us'] for o in r['observer'] if o['region'] == 'measurement']) + for r in reports], 'semantics': SEMANTICS} + + +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_DIAG_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_DIAG_REPORT=")] + closes = [s.partition("=")[2] for s in raw.splitlines() if s.startswith("P28_DIAG_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 digest(value): + return hashlib.sha256(json.dumps(value, sort_keys=True, separators=(',', ':')).encode()).hexdigest() + + +def check_source_maps(before, after): + require(before == after and len(before) == 1407, 'source map mismatch') + for name, ref in before.items(): + require(not Path(name).is_absolute() and '..' not in Path(name).parts and + set(ref) == {'bytes', 'sha256'}, 'source map path/ref') + hash_value(ref['sha256']) + integer(ref['bytes'], 'source bytes') + + +def check_receipt(receipt, out): + from diag_p28_02 import COLLECTOR_PINS + out = Path(out) + require(receipt['passed'] is True and receipt['error'] is None and receipt['cleanup_errors'] == [], + 'execution failed') + source = receipt['source_before'] + check_source_maps(source, receipt['source_after']) + require(receipt['default_player_before'] == receipt['default_player_after'], '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']) == LOCAL_NAMES, + 'worker changed') + require(receipt['owned_writes'] == [] and receipt['original_credit'] == 0 and + receipt['formal_completed'] == 23 and receipt['denominator'] == 88 and + receipt['packages_completed'] == 4 and receipt['package_denominator'] == 30 and + receipt['budget_false_count_unchanged'] == 6, 'scope or credit changed') + admission = receipt['admission'] + require(json.loads((out / 'admission.json').read_text()) == admission, 'admission raw') + require(admission['source_map'] == source and admission['metadata']['source_map_sha256'] == digest(source) + and admission['metadata']['source_count'] == 1407 and + admission['metadata']['worker_pins'] == receipt['worker_before'], 'admission source binding') + require(admission['fixture_sha256'] == FIXTURE_SHA and admission['dependencies'] == DEPENDENCIES and + admission['collector_pins'] == COLLECTOR_PINS and admission['phases'] == PHASES, + 'admission inputs') + require(set(receipt['script_pins']) == {'diag_probe.gd', 'profile_guard.gd', 'guarded.gd'}, 'script inventory') + for name, ref in receipt['script_pins'].items(): + require(pin(out / name) == ref, 'executed script differs') + require(sha(out / 'diag_probe.gd') == receipt['worker_before']['diag_probe.gd'], 'probe differs from S') + require(sha(out / 'profile_guard.gd') == admission['readonly']['files']['run-r02/profile_guard.gd'], + 'guard differs from pinned input') + manifest_path = out / 'root-input-manifest.json' + require(sha(manifest_path) == admission['readonly']['manifest_sha256'] == + '9dac0a51d52087e691c5723b4f78f171f7998c78b582b2b6c4c904798e77644d', 'Root input manifest pin') + guard_refs = [r for r in json.loads(manifest_path.read_text())['files'] + if r['destination'].endswith('/run-r02/profile_guard.gd')] + require(len(guard_refs) == 1 and guard_refs[0]['sha256'] == sha(out / 'profile_guard.gd'), + 'guard not bound to original Root manifest') + script_path = Path(admission['planned_argv'][2][-1]) + wrapper = 'extends ' + json.dumps(str(script_path.parent / 'diag_probe.gd')) + '\nconst Guard = preload(' + \ + json.dumps(str(script_path.parent / 'profile_guard.gd')) + ')\nfunc _initialize() -> void:\n' + \ + '\tif not Guard.activate():\n\t\tquit(97)\n\t\treturn\n\tsuper._initialize()\n' + require((out / 'guarded.gd').read_text() == wrapper, 'guard wrapper differs') + check_probe((out / 'diag_probe.gd').read_text()) + rows = receipt['phases'] + require([r['phase'] for r in rows] == PHASES, 'phase order/incomplete execution') + require(len(admission['planned_argv']) == len(PHASES), 'phase plan') + pids, profiles = [], [] + previous = source + reports = [] + for index, row in enumerate(rows): + require('--headless' in row['argv'], 'native run not headless') + require(row['argv'][:-3] == admission['planned_argv'][index] and + row['argv'][-3] == '--log-file' and row['argv'][-1] == '--verbose', 'argv differs from plan') + report = bound_raw(out / row['phase'], row) + pids.extend(row['capture']['actual_captured_pids']) + current = row['stage_inventory'] + require(row['stage_before'] == digest(previous) and row['stage_after'] == digest(current), + 'stage inventory digest') + require(all(current.get(k) == v for k, v in previous.items()), 'stage source changed') + added = set(current) - set(previous) + require(not added or row['phase'] == 'import' and all( + (n.endswith('.gd.uid') and n[:-4] in previous) or + (n.endswith('.import') and n[:-7] in previous) for n in added), 'unexpected stage files') + previous = current + require(row['stage_preserved'] is True and row['source_preserved'] is True and + row['player_preserved'] is True, 'phase preservation') + startup = row['startup_project'] + startup_path = out / (row['phase'] + '-startup-project.godot') + require(startup['fresh_before'] is True and sha(startup_path) == startup['sha256'] and + startup['restored_sha256'] == source['project.godot']['sha256'], 'startup profile') + profiles.append(startup['profile']) + if report: + require(report['metadata'] == admission['metadata'], 'report build/machine metadata differs') + require(json.loads((out / (row['phase'] + '-report.json')).read_text()) == report, 'report copy differs') + reports.append(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') + profiles.append(row['runtime_profile']) + require(len(set(pids)) == len(pids) and len(set(profiles)) == len(profiles), 'reused PID/profile') + 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') + summary = compare_runs(reports) + require(receipt['summary'] == summary, 'summary/ranking differs from raw') + return summary + + diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/diag_p28_02.py b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/diag_p28_02.py new file mode 100644 index 0000000..d365dda --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/diag_p28_02.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +"""Root-only three-PID headless DIAG 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_diag 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 ("diag_p28_02.py", "check_diag.py", "verify_diag.py", "test_diag.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("diag_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 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 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 / 'diag_probe.gd'), + collector.parse_argv(engine, stage, wrapper)] + [ + [str(engine), '--headless', '--path', str(stage), '--script', str(wrapper)] for _ in range(3)] + + +def schedule(rows): + for index, label in enumerate(check.PHASES): + check.require(all(row['passed'] is True for row in rows), 'previous phase failed') + 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 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) == check.FIXTURE_SHA and check.sha(engine) == check.ENGINE_SHA, + 'fixture/engine SHA') + check.check_probe((HERE / 'diag_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) + # Frozen S must contain the submitted diagnostic too, not just product files. + for name in check.LOCAL_NAMES: + relative = (HERE / name).relative_to(ROOT).as_posix() + committed = subprocess.check_output(['git', '-C', str(ROOT), 'show', commit + ':' + relative]) + check.require(committed == (HERE / name).read_bytes(), 'diagnostic differs from frozen S: ' + name) + worker_pins = {name: check.sha(HERE / name) for name in sorted(check.LOCAL_NAMES)} + commands = plan(collector, engine, out) + check.require(sys.platform == 'darwin', 'reviewed native platform is macOS') + machine = subprocess.check_output(['sysctl', '-n', 'hw.model'], text=True).strip() + check.require(machine == 'Mac16,10', 'machine differs from R') + metadata = {'commit': commit, 'measured_source': 'S', 'candidate_Q_measured': False, + 'engine_sha256': check.sha(engine), 'engine_version': collector.VERSION, + 'machine': machine, 'headless': True, 'source_count': len(source), + 'source_map_sha256': collector.digest(source), 'worker_pins': worker_pins} + admission = {'metadata': metadata, 'source_map': source, 'readonly': inputs, 'planned_argv': commands, + 'fixture_sha256': check.sha(fixture), 'dependencies': check.DEPENDENCIES, + 'collector_pins': pinned, '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 + collector.no_live_engine() + player = standard.default_player(ROOT) + out.mkdir(parents=True) + (out / 'root-input-manifest.json').write_bytes((readonly / 'ROOT_INPUT_MANIFEST.json').read_bytes()) + 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 ('diag_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 = {p.name: check.pin(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.pin(out / 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 + '-diag-startup' + runtime_name = 'TheTree-PCK-tests-' + uuid.uuid4().hex + '-diag-runtime' + startup, runtime = (standard.profile_path(n) for n in (startup_name, runtime_name)) + check.require(not startup.exists() and not runtime.exists(), 'profile must be fresh') + profiles.extend([startup, runtime]) + 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_DIAG_ROLE': label, 'THE_TREE_DIAG_META': json.dumps(metadata), + 'THE_TREE_P28_FIXTURE': str(fixture), 'THE_TREE_P28_FIXTURE_SHA': check.FIXTURE_SHA} + 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, + stage_inventory=after_stage, + 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)) + raw = (out / label / 'stdout.log').read_text() + reports = [s.partition('=')[2] for s in raw.splitlines() if s.startswith('P28_DIAG_REPORT=')] + check.require(len(reports) == 1, 'one report required') + row['report'] = json.loads(reports[0]) + collector.write(out / (label + '-report.json'), row['report']) + check.bound_raw(out / label, row) + check.require(row['source_preserved'] and row['player_preserved'] and + check.sha(fixture) == check.FIXTURE_SHA and check.sha(engine) == check.ENGINE_SHA and + all(check.pin(out / 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': [], + 'original_credit': 0, 'formal_completed': 23, 'denominator': 88, + 'packages_completed': 4, 'package_denominator': 30, 'budget_false_count_unchanged': 6} + if receipt['passed']: + try: + receipt['summary'] = check.compare_runs([row['report'] for row in rows[3:]]) + 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) + if receipt['passed']: + from verify_diag import seal + seal(out) + 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-02/diag/diag_probe.gd b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/diag_probe.gd new file mode 100644 index 0000000..93687ed --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/diag_probe.gd @@ -0,0 +1,424 @@ +extends SceneTree +## External passive diagnostic. Root alone executes this through the profile guard. +const GameStateScript = preload("res://autoload/game_state.gd") +const SERVICES := ["weather", "command_queue", "id_generator", "domain_events", "world_service", + "path_service", "mana_network_service", "mana_reserve", "economy_service", "facility_upkeep", + "upkeep_settlement", "inventory_service", "kitchen_production", "auto_purchase_plans", "elevators", + "legacy_stairs", "logistics_dispatcher", "facility_service", "facility_cleanliness", "guest_service", + "staff_service", "progression_service", "relationship_service", "event_service", "authority_service", + "defense_service", "construction_service", "excavation_service", "spatial_subslot_service", + "route_subslot_path_adapter", "facility_infrastructure_projector", "peeping_room_service"] +const K_FIELDS := ["absolute_minute", "speed", "paused", "snapshot_version", "_skip_next_process", + "_pause_request_serial", "_abort_current_tick_requested", "_last_tick_advanced"] +var clock: Variant +var runtime: Variant +var main: Variant +var report: Dictionary = {} +var ticks: Array = [] +var queries_outside: Array = [] +var observer: Array = [] +var saves: Array = [] +var active: Dictionary = {} +var interval := -1 +var region := "preparation" +var next_tick_id := 0 +var last_completed := -1 +var connections_before: Dictionary = {} +var failed: Array = [] +var finished := false +var cache_preparations: Array = [] + +func _initialize() -> void: + call_deferred("begin") + +func hashed(data: PackedByteArray) -> String: + var context := HashingContext.new() + context.start(HashingContext.HASH_SHA256) + context.update(data) + return context.finish().hex_encode() + +func demand(ok: bool, reason: String) -> void: + if not ok: + failed.append(reason) + +func kernel_state() -> Dictionary: + var result := {} + for key: String in K_FIELDS: + result[key] = clock.get(key) + var f64 := PackedByteArray() + f64.resize(8) + f64.encode_double(0, float(clock.get("_accumulator"))) + result["_accumulator_f64_hex"] = f64.hex_encode() + var handlers: Dictionary = clock.get("_phase_handlers") + var handler_ids := {} + for phase: int in range(11): + var names: Array = [] + for key: Variant in handlers.get(phase, {}): + names.append(str(key)) + names.sort() + handler_ids[str(phase)] = names + result["handlers"] = handler_ids + var budget: Variant = clock.get("performance_budget") + for key: String in ["_sample_counts", "_totals", "_samples"]: + result[key] = budget.get(key).duplicate(true) + return result + +func cache_state() -> Dictionary: + var catalog: Variant = runtime.get("_mvp_manifest_cache_catalog") + var current: Variant = runtime.get("mvp_content_catalog") + var value: Dictionary = runtime.get("_mvp_manifest_cache") + return {"generation": runtime.get("_mvp_manifest_cache_generation"), + "catalog_id": str(catalog.get_instance_id()) if catalog != null else null, + "current_catalog_id": str(current.get_instance_id()), "current_generation": current.get_manifest_generation(), + "bytes_sha256": hashed(var_to_bytes(value)), + "filled": catalog == current and runtime.get("_mvp_manifest_cache_generation") == current.get_manifest_generation()} + +func ensure_cache(label: String) -> void: + var before := cache_state() + if not before.filled: + runtime.create_snapshot() + cache_preparations.append({"label": label, "before": before, "after": cache_state(), + "filled_outside_sample": not before.filled}) + demand(cache_state().filled, "cache preparation failed") + +func layers() -> Dictionary: + # Read C/K before the S observation so a cache mutation cannot be hidden. + var k := kernel_state() + var c := cache_state() + var snapshot: Dictionary = runtime.create_snapshot() + var s := var_to_bytes(snapshot) + demand(var_to_bytes(k) == var_to_bytes(kernel_state()), "state witness changed K") + demand(var_to_bytes(c) == var_to_bytes(cache_state()), "state witness changed C") + return {"S": hashed(s), "K": hashed(var_to_bytes(k)), "C": hashed(var_to_bytes(c)), + "kernel": k, "cache": c, "snapshot": snapshot, "s_bytes": s, + "k_bytes": var_to_bytes(k), "c_bytes": var_to_bytes(c)} + +func brief(value: Dictionary) -> Dictionary: + return {"S": value.S, "K": value.K, "C": value.C, "kernel": value.kernel, "cache": value.cache} + +func typed(value: Variant) -> Variant: + if value is Dictionary: + var keys: Array = value.keys() + keys.sort_custom(func(a: Variant, b: Variant) -> bool: return str(a) < str(b)) + var pairs: Array = [] + for index: int in range(keys.size()): + if index > 0: + demand(str(keys[index - 1]) != str(keys[index]), "typed key collision") + pairs.append([typed(keys[index]), typed(value[keys[index]])]) + return [typeof(value), pairs] + if value is Array: + var children: Array = [] + for child: Variant in value: + children.append(typed(child)) + return [typeof(value), children] + return [typeof(value), var_to_bytes(value).hex_encode()] + +func diff_paths(a: Variant, b: Variant, path: String = "$") -> Array: + if var_to_bytes(a) == var_to_bytes(b): + return [] + var result: Array = [] + if a is Dictionary and b is Dictionary: + if var_to_bytes(typed(a)) == var_to_bytes(typed(b)): + result.append({"path": path, "kind": "dictionary_enumeration_order_only"}) + var keys: Array = a.keys() + for key: Variant in b: + if not keys.has(key): + keys.append(key) + for key: Variant in keys: + var child_path := path + "/" + str(typeof(key)) + ":" + str(key) + if not a.has(key) or not b.has(key): + result.append({"path": child_path, "kind": "missing_key"}) + else: + result.append_array(diff_paths(a[key], b[key], child_path)) + elif a is Array and b is Array and a.size() == b.size(): + for index: int in range(a.size()): + result.append_array(diff_paths(a[index], b[index], path + "/" + str(index))) + else: + result.append({"path": path, "kind": "type_or_value"}) + return result + +func comparison(before: Dictionary, after: Dictionary) -> Dictionary: + var equal := {"S": before.s_bytes == after.s_bytes, "K": before.k_bytes == after.k_bytes, + "C": before.c_bytes == after.c_bytes} + var differences: Array = [] + if not equal.S: + differences = diff_paths(before.snapshot, after.snapshot) + var rebuilt_matches: Variant = null + if not equal.C: + # Public catalog reads reconstruct the exact finite manifest fields from + # simulation_runtime.gd:11201-11214; never call its private builder. + var catalog: Variant = runtime.get("mvp_content_catalog") + var guest_ids: Array[String] = [] + for definition: GuestDefinition in catalog.build_all_guest_definitions(): + guest_ids.append(String(definition.guest_type_id)) + var expected := {&"manifest_version": SimulationRuntimeService.MVP_CONTENT_MANIFEST_VERSION, + &"schema_version": MvpContentCatalog.SCHEMA_VERSION, + &"guest_class_ids": string_names(catalog.get_guest_class_ids()), + &"species_ids": string_names(catalog.get_species_ids()), + &"staff_candidate_ids": string_names(catalog.get_staff_candidate_ids()), + &"unique_character_ids": string_names(catalog.get_unique_character_ids()), + &"guest_definition_ids": guest_ids} + rebuilt_matches = var_to_bytes(expected) == var_to_bytes(runtime.get("_mvp_manifest_cache")) + demand(rebuilt_matches, "cache differs from current catalog manifest") + demand(equal.S and equal.K and equal.C, "S/K/C changed outside tick") + return {"before": brief(before), "after": brief(after), "equal": equal, + "differences": differences, "cache_miss_in_sample": not equal.C, + "cache_rebuilt_matches_catalog": rebuilt_matches, + "cache_rebuild_check_reason": "no_cache_miss" if equal.C else null} + +func string_names(values: Array[StringName]) -> Array[String]: + var result: Array[String] = [] + for value: StringName in values: + result.append(String(value)) + return result + +func signal_inventory() -> Dictionary: + var result := {} + var signals: Array = [clock.tick_started, clock.tick_phase_started, clock.tick_completed, + runtime.path_service.path_query_measured, runtime.save_operation_completed] + for sig: Signal in signals: + var items: Array = [] + for connection: Dictionary in sig.get_connections(): + var callback: Callable = connection.callable + items.append({"object": str(callback.get_object_id()), "method": str(callback.get_method()), + "flags": connection.flags}) + items.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return JSON.stringify(a) < JSON.stringify(b)) + result[str(sig.get_name())] = {"count": items.size(), "targets": items} + return result + +func observe_start(minute: int) -> void: + var started := Time.get_ticks_usec() + next_tick_id += 1 + demand(active.is_empty(), "overlapping ticks") + active = {"id": next_tick_id, "region": region, "start_minute": minute, "edges": [], "queries": [], + "frame_wall_before_us": started - last_completed if last_completed >= 0 else null} + active.edges.append({"name": "tick_started", "minute": minute, "at_us": started}) + interval = 0 + observer.append({"callback": "tick_started", "tick_id": next_tick_id, "region": region, + "duration_us": Time.get_ticks_usec() - started}) + +func observe_phase(phase: int, minute: int) -> void: + var started := Time.get_ticks_usec() + demand(not active.is_empty(), "phase without tick") + active.edges.append({"name": "phase", "phase": phase, "minute": minute, "at_us": started}) + interval = phase + 1 + observer.append({"callback": "tick_phase_started", "tick_id": next_tick_id, "region": region, + "duration_us": Time.get_ticks_usec() - started}) + +func observe_completed(minute: int) -> void: + var started := Time.get_ticks_usec() + active.edges.append({"name": "tick_completed", "minute": minute, "at_us": started}) + active["end_minute"] = minute + ticks.append(active) + active = {} + interval = -1 + last_completed = started + observer.append({"callback": "tick_completed", "tick_id": next_tick_id, "region": region, + "duration_us": Time.get_ticks_usec() - started}) + +func observe_query(duration_us: int) -> void: + var started := Time.get_ticks_usec() + var item := {"duration_us": duration_us, "at_us": started, "tick_id": next_tick_id if interval >= 0 else null, + "interval": interval if interval >= 0 else null, "region": region} + if interval >= 0: + active.queries.append(item) + else: + queries_outside.append(item) + observer.append({"callback": "path_query_measured", "tick_id": item.tick_id, "region": region, + "duration_us": Time.get_ticks_usec() - started}) + +func observe_save(result: Dictionary) -> void: + var started := Time.get_ticks_usec() + saves.append({"at_us": started, "tick_id": next_tick_id if interval >= 0 else null, + "interval": interval if interval >= 0 else null, "region": region, "result": result.duplicate(true)}) + observer.append({"callback": "save_operation_completed", "tick_id": next_tick_id if interval >= 0 else null, + "region": region, "duration_us": Time.get_ticks_usec() - started}) + +func step_window(label: String) -> void: + region = label + for index: int in range(30): + var before: int = clock.absolute_minute + demand(clock.paused, "clock resumed") + clock.advance_minutes(1) + demand(clock.absolute_minute == before + 1 and clock.paused, "manual step did not advance one minute paused") + await process_frame + demand(clock.absolute_minute == before + 1 and clock.paused, "inter-tick frame advanced clock") + if Time.get_ticks_msec() > 1740000 or not failed.is_empty(): + failed.append("window stopped") + return + +func snapshot_samples(name: String, target: Variant) -> void: + var rows: Array = [] + for index: int in range(5): + var before := layers() + var started := Time.get_ticks_usec() + var value: Dictionary = target.create_snapshot() + var duration := Time.get_ticks_usec() - started + var after := layers() + var row := comparison(before, after) + row["duration_us"] = duration + row["result_sha256"] = hashed(var_to_bytes(value)) + rows.append(row) + report.measurements[name] = {"samples": rows, "reason": null, "scope": "independent_inclusive"} + +func scratch_samples() -> void: + for operation: String in ["deep_copy", "envelope_validate", "envelope_restore", "save", "load"]: + var rows: Array = [] + for index: int in range(5): + var before := layers() + var scratch: Dictionary = before.snapshot.duplicate(true) + var scratch_clock := SimulationKernel.new() + var scratch_state := GameStateScript.new() + var ids := [scratch_clock.get_instance_id(), scratch_state.get_instance_id(), scratch_clock.performance_budget.get_instance_id()] + var path := "user://p28_diag/scratch/" + operation + "-" + str(index) + if operation == "load": + var prepared: Dictionary = SaveService.save_snapshot(path, scratch) + demand(prepared.get(&"ok", false), "scratch load preparation failed") + var started := Time.get_ticks_usec() + var result: Variant + match operation: + "deep_copy": result = scratch.duplicate(true) + "envelope_validate": result = SimulationSnapshot.validate(scratch) + "envelope_restore": result = SimulationSnapshot.restore(scratch, scratch_clock, scratch_state) + "save": result = SaveService.save_snapshot(path, scratch) + "load": result = SaveService.load_snapshot(path) + var duration := Time.get_ticks_usec() - started + var ok: bool = result if result is bool else true if operation == "deep_copy" else result.get(&"ok", false) + demand(ok, "scratch operation failed: " + operation) + # Mutate only detached caller-owned containers, then destroy scratch objects. + scratch[&"game_state"][&"resources"][&"gold"] = -987654 + if result is Dictionary: + result[&"diag_detached_sentinel"] = true + result = null + scratch = {} + scratch_clock.free() + scratch_state.free() + scratch_clock = null + scratch_state = null + var released: Array = [] + var id_text: Array = [] + for id: int in ids: + id_text.append(str(id)) + released.append(not is_instance_id_valid(id)) + demand(not is_instance_id_valid(id), "scratch object retained") + var after := layers() + var row := comparison(before, after) + row.merge({"duration_us": duration, "ok": ok, "scratch_path": path, "scratch_ids": id_text, + "released": released, "input": "live_snapshot_deep_copy", "detached_mutation_checked": true}) + rows.append(row) + report.measurements[operation] = {"samples": rows, "reason": null, "scope": "independent_inclusive"} + report.measurements["runtime_restore"] = {"samples": null, "reason": "Runtime restore requires initialize(clock,state,seed) before restore; initialize is outside the listed scratch restore/save/load/validator API set. Envelope restore above does not restore Runtime systems.", "scope": "unmeasured"} + report.measurements["runtime_validation_internal"] = {"samples": null, "reason": "Full Runtime validation is internal to restore; no public standalone validator. Envelope validation is measured separately and is not Runtime validation.", "scope": "unmeasured"} + report.measurements["connector_rebuild"] = {"samples": null, "reason": "path_query_measured observes queries only; no passive connector rebuild timing signal.", "scope": "unmeasured"} + +func begin() -> void: + clock = root.get_node("GameClock") + runtime = root.get_node("SimulationRuntime") + report = {"schema": "p28-02-diag-v1", "role": OS.get_environment("THE_TREE_DIAG_ROLE"), + "pid": OS.get_process_id(), "metadata": JSON.parse_string(OS.get_environment("THE_TREE_DIAG_META")), + "semantics": {"intervals": "inclusive_wall_including_observer", "observer_subtracted": false, + "breakdown": "independent_inclusive_not_additive", "path": "query_only_not_connector"}, + "observer_note": "Callback wall through final timestamp; excludes appending its own timing record and return. Included in intervals, never subtracted.", + "measurements": {}, "preparation": [], "baselines": {}, "seed": str(runtime.world_seed)} + var k0 := kernel_state() + clock.set_paused(true) + report.preparation.append({"operation": "initial_pause", "K_before": k0, "K_after": kernel_state()}) + var path := OS.get_environment("THE_TREE_P28_FIXTURE") + report["input_sha256"] = FileAccess.get_sha256(path) + if report.input_sha256 != OS.get_environment("THE_TREE_P28_FIXTURE_SHA"): + failed.append("fixture SHA") + finish() + return + var input := FileAccess.open(path, FileAccess.READ) + if input == null: + failed.append("fixture open") + finish() + return + var snapshot: Variant = input.get_var(false) + input.close() + var restored: bool = runtime.restore_snapshot(snapshot) + demand(restored, "fixture restore") + var restored_cache := cache_state() + ensure_cache("restored") + report.baselines["restored"] = brief(layers()) + report.preparation.append({"operation": "fixture_restore", "ok": restored, "cache_before_snapshot": restored_cache, + "state": report.baselines.restored}) + k0 = kernel_state() + clock.set_paused(true) + report.preparation.append({"operation": "restored_pause", "K_before": k0, "K_after": kernel_state()}) + var before_main := brief(layers()) + main = load("res://scenes/main/main.tscn").instantiate() + root.add_child(main) + var after_main := brief(layers()) + await process_frame + var frame1 := brief(layers()) + await process_frame + var frame2 := brief(layers()) + report["startup"] = {"before_main": before_main, "after_main": after_main, "frame1": frame1, "frame2": frame2, + "save_policy": runtime.get_save_policy(), "ops": main.get("_operations_start_report").duplicate(true)} + demand(report.startup.ops.get(&"code", "") == "INVALID_START_TIME", "unexpected startup Ops result") + demand(clock.paused and clock.absolute_minute == 602, "B0 clock") + report.baselines["B0"] = brief(layers()) + connections_before = signal_inventory() + report["observer_before"] = signal_inventory() + clock.tick_started.connect(observe_start) + clock.tick_phase_started.connect(observe_phase) + clock.tick_completed.connect(observe_completed) + runtime.path_service.path_query_measured.connect(observe_query) + runtime.save_operation_completed.connect(observe_save) + report["observer_connected"] = signal_inventory() + await step_window("warm") + ensure_cache("B1") + report.baselines["B1"] = brief(layers()) + demand(clock.absolute_minute == 632, "B1 minute") + if failed.is_empty(): + await step_window("measurement") + region = "outside_tick" + ensure_cache("outside_tick") + var before := layers() + report.baselines["outside_before"] = brief(before) + report["seed"] = str(runtime.world_seed) + var rng := {} + for key: Variant in runtime.get("_rng_streams"): + # Preserve int64 state without JSON floating point truncation. + rng[str(key)] = var_to_bytes(runtime.get("_rng_streams")[key].snapshot()).hex_encode() + report["rng_state_typed_hex"] = rng + if failed.is_empty(): + snapshot_samples("runtime_snapshot", runtime) + snapshot_samples("game_state_snapshot", root.get_node("GameState")) + for service: String in SERVICES: + snapshot_samples(service + "_snapshot", runtime.get(service)) + scratch_samples() + # Test ownership of the returned manifest without mutating live state. + var detached: Dictionary = runtime.create_snapshot() + detached[&"systems"][&"runtime"][&"catalog_manifest"][&"diag_detached_sentinel"] = true + detached[&"systems"][&"runtime"][&"catalog_manifest"][&"guest_class_ids"].append("diag_detached_sentinel") + report["manifest_detached"] = comparison(before, layers()) + report["outside_total"] = comparison(before, layers()) + clock.tick_started.disconnect(observe_start) + clock.tick_phase_started.disconnect(observe_phase) + clock.tick_completed.disconnect(observe_completed) + runtime.path_service.path_query_measured.disconnect(observe_query) + runtime.save_operation_completed.disconnect(observe_save) + report["observer_disconnected"] = signal_inventory() + demand(report.observer_before == report.observer_disconnected, "observer connections not restored") + report["all_connections_restored"] = connections_before == signal_inventory() + demand(report.all_connections_restored, "save observer not restored") + demand(clock.paused and clock.absolute_minute == 662, "final clock") + finish() + +func finish() -> void: + if finished: + return + finished = true + report.merge({"ticks": ticks, "queries_outside": queries_outside, "observer": observer, "save_events": saves, + "cache_preparations": cache_preparations, "errors": failed, "stop_reason": "complete" if failed.is_empty() else "failed"}) + print("P28_DIAG_REPORT=" + JSON.stringify(report)) + if is_instance_valid(main): + main.ui_audio_window_close_drained.connect(audio_close, CONNECT_ONE_SHOT) + root.close_requested.emit() + else: + quit(1) + +func audio_close(result: Dictionary) -> void: + print("P28_DIAG_AUDIO_CLOSE=" + JSON.stringify(result)) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/test_diag.py b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/test_diag.py new file mode 100644 index 0000000..b6eff10 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/test_diag.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +"""Engine-free tests. All report values below are synthetic, never native evidence.""" +import copy +import json +import os +import re +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_diag as check +import diag_p28_02 as driver +import verify_diag as verify + +H = 'a' * 64 + + +def layers(minute=662): + kernel = {name: False for name in check.K_FIELDS} + kernel.update(absolute_minute=minute, speed=1, paused=True, snapshot_version=minute, + _pause_request_serial=2, _accumulator_f64_hex='0000000000000000', + handlers={str(i): ['handler'] for i in range(11)}, _sample_counts={}, _totals={}, _samples={}) + return {'S': H, 'K': H, 'C': H, 'kernel': kernel, + 'cache': {'generation': 1, 'current_generation': 1, 'catalog_id': '10', 'current_catalog_id': '10', + 'bytes_sha256': H, 'filled': True}} + + +def comparison(): + return {'before': layers(), 'after': layers(), 'equal': {'S': True, 'K': True, 'C': True}, + 'differences': [], 'cache_miss_in_sample': False, 'cache_rebuilt_matches_catalog': None, + 'cache_rebuild_check_reason': 'no_cache_miss'} + + +def synthetic_report(pid=1, role='diag-1'): + connections = {name: {'count': 0, 'targets': []} for name in + ('tick_started', 'tick_phase_started', 'tick_completed', 'path_query_measured', + 'save_operation_completed')} + connected = copy.deepcopy(connections) + for name in connected: + connected[name] = {'count': 1, 'targets': [{'object': '1', 'method': 'observer', 'flags': 0}]} + result = {'schema': 'p28-02-diag-v1', 'role': role, 'pid': pid, 'errors': [], 'stop_reason': 'complete', + 'semantics': copy.deepcopy(check.SEMANTICS), 'input_sha256': check.FIXTURE_SHA, + 'metadata': {'engine_sha256': check.ENGINE_SHA, 'engine_version': '4.7.2.stable.official.ed1daf0bf', + 'machine': 'Mac16,10', 'headless': True, 'measured_source': 'S', + 'candidate_Q_measured': False, 'commit': 'b' * 40}, + 'seed': '123', 'rng_state_typed_hex': {'world': '0001'}, + 'baselines': {name: layers(minute) for name, minute in + [('restored', 602), ('B0', 602), ('B1', 632), ('outside_before', 662)]}, + 'startup': {name: layers(602) for name in ('before_main', 'after_main', 'frame1', 'frame2')}, + 'preparation': [], 'ticks': [], 'queries_outside': [], 'observer': [], 'save_events': [], + 'measurements': {}, 'outside_total': comparison(), 'manifest_detached': comparison(), + 'observer_before': connections, 'observer_connected': connected, + 'observer_disconnected': copy.deepcopy(connections), 'all_connections_restored': True, + 'cache_preparations': [{'label': name, 'before': layers()['cache'], 'after': layers()['cache'], + 'filled_outside_sample': False} for name in ('restored', 'B1', 'outside_tick')]} + result['startup']['ops'] = {'code': 'INVALID_START_TIME'} + result['startup']['save_policy'] = {'enabled': True} + for operation in ('initial_pause', 'fixture_restore', 'restored_pause'): + if operation == 'fixture_restore': + row = {'operation': operation, 'ok': True, 'state': layers(602), 'cache_before_snapshot': layers()['cache']} + else: + row = {'operation': operation, 'K_before': layers()['kernel'], 'K_after': layers()['kernel']} + row['K_before']['_pause_request_serial'] = 1 + result['preparation'].append(row) + for index in range(60): + minute, at, region = 602 + index, index * 200, 'warm' if index < 30 else 'measurement' + edges = [{'name': 'tick_started', 'minute': minute, 'at_us': at}] + edges += [{'name': 'phase', 'phase': phase, 'minute': minute + (phase != 0), + 'at_us': at + 10 * (phase + 1)} for phase in range(11)] + edges.append({'name': 'tick_completed', 'minute': minute + 1, 'at_us': at + 120}) + query = {'duration_us': 0, 'at_us': at + 115, 'interval': 11, 'tick_id': index + 1, 'region': region} + result['ticks'].append({'id': index + 1, 'region': region, 'start_minute': minute, 'end_minute': minute + 1, + 'edges': edges, 'queries': [query], 'frame_wall_before_us': 80 if index else None}) + for callback in ['tick_started'] + ['tick_phase_started'] * 11 + ['tick_completed', 'path_query_measured']: + result['observer'].append({'callback': callback, 'tick_id': index + 1, 'region': region, 'duration_us': 0}) + snapshots = {'runtime_snapshot', 'game_state_snapshot'} | {s + '_snapshot' for s in check.SERVICES} + for name in snapshots | check.SCRATCH: + samples = [] + for index in range(5): + row = comparison() + row['duration_us'] = index + if name in snapshots: + row['result_sha256'] = H + else: + row.update(ok=True, input='live_snapshot_deep_copy', detached_mutation_checked=True, + scratch_path='user://p28_diag/scratch/' + name + '-' + str(index), + scratch_ids=['100', '101', '102'], released=[True] * 3) + samples.append(row) + result['measurements'][name] = {'samples': samples, 'reason': None, 'scope': 'independent_inclusive'} + for name in check.UNMEASURED: + result['measurements'][name] = {'samples': None, 'reason': 'synthetic unmeasurable public boundary', + 'scope': 'unmeasured'} + return result + + +class DiagnosticTests(unittest.TestCase): + def reject(self, mutate): + report = synthetic_report() + mutate(report) + with self.assertRaises((ValueError, KeyError, TypeError)): + check.check_report(report) + + def test_valid_raw_intervals(self): + summary = check.check_report(synthetic_report()) + self.assertEqual(summary['intervals']['I0'], {'count': 30, 'mean_us': 10, 'p95_us': 10, 'max_us': 10}) + self.assertEqual(summary['query_only']['I11']['count'], 30) + self.assertEqual(summary['query_only']['I11']['total_us'], 0) + self.assertIsNone(summary['query_only']['I0']['stats']) + self.assertEqual(summary['inter_tick_frame_wall']['mean_us'], 80) + + def test_tick_id_and_minute_rejection(self): + for mutate in (lambda r: r['ticks'][0]['edges'][2].update(minute=602), + lambda r: r['ticks'][1].update(id=1), + lambda r: r['ticks'][0]['edges'].pop(2), + lambda r: r['ticks'][30].update(region='warm'), + lambda r: r['ticks'][30].update(frame_wall_before_us=0), + lambda r: r['ticks'].pop()): + with self.subTest(mutate=mutate): + self.reject(mutate) + + def test_queries_and_observer_rejection(self): + for mutate in (lambda r: r['ticks'][0]['queries'][0].update(interval=10), + lambda r: r['ticks'][0]['queries'][0].update(tick_id=2), + lambda r: r['ticks'][0]['queries'][0].update(duration_us=None), + lambda r: r['observer'].pop(0), + lambda r: r['observer'][0].update(duration_us=-1), + lambda r: r['queries_outside'].append({'tick_id': 1, 'interval': None, 'duration_us': 0})): + with self.subTest(mutate=mutate): + self.reject(mutate) + + def test_nonadditive_scope(self): + for key, value in [('intervals', 'exclusive'), ('observer_subtracted', True), + ('breakdown', 'sum_to_100_percent'), ('path', 'connector')]: + with self.subTest(key=key): + self.reject(lambda r: r['semantics'].update({key: value})) + + def test_state_and_cache_mismatch(self): + for layer in ('S', 'K', 'C'): + self.reject(lambda r: r['outside_total']['equal'].update({layer: False})) + self.reject(lambda r: r['outside_total']['after'].update({layer: 'b' * 64})) + self.reject(lambda r: r['outside_total'].update(cache_miss_in_sample=True)) + self.reject(lambda r: r['outside_total']['before']['cache'].update(filled=False)) + self.reject(lambda r: r['manifest_detached']['after']['kernel'].update(_samples={'frame': [1]})) + self.reject(lambda r: r['outside_total']['before']['kernel'].pop('_accumulator_f64_hex')) + + def test_missing_not_zero(self): + self.assertEqual(check.stats([0] * 5)['mean_us'], 0) + for value in ([], [None], [True], [-1], [float('nan')]): + with self.subTest(value=value), self.assertRaises(ValueError): + check.stats(value) + self.reject(lambda r: r['measurements']['runtime_restore'].update(samples=[0] * 5)) + self.reject(lambda r: r['measurements']['runtime_restore'].update(reason='')) + self.reject(lambda r: r['measurements']['save']['samples'].pop()) + + def test_scratch_ownership(self): + for mutate in (lambda s: s.update(input='live'), lambda s: s.update(released=[True, False, True]), + lambda s: s.update(scratch_path='user://saves/player'), + lambda s: s.update(detached_mutation_checked=False), lambda s: s.update(ok=False)): + self.reject(lambda r: mutate(r['measurements']['save']['samples'][0])) + + def test_connections_and_preparation(self): + self.reject(lambda r: r.update(all_connections_restored=False)) + self.reject(lambda r: r['observer_disconnected']['tick_started'].update(count=1)) + self.reject(lambda r: r['baselines']['B1']['kernel'].update(absolute_minute=631)) + self.reject(lambda r: r['preparation'][2]['K_after'].update(_pause_request_serial=1)) + self.reject(lambda r: r['startup']['ops'].update(code='OK')) + + def test_three_pid_comparison(self): + reports = [synthetic_report(i + 1, 'diag-' + str(i + 1)) for i in range(3)] + self.assertTrue(check.compare_runs(reports)['rankings_agree']) + reports[1]['pid'] = 1 + with self.assertRaises(ValueError): + check.compare_runs(reports) + reports[1]['pid'] = 2 + reports[1]['baselines']['B0']['S'] = 'b' * 64 + with self.assertRaisesRegex(ValueError, 'preparation mismatch'): + check.compare_runs(reports) + + def test_pid_local_catalog_identity(self): + a, b = synthetic_report(), synthetic_report(2, 'diag-2') + b['baselines']['B0']['cache'].update(catalog_id='20', current_catalog_id='20') + b['baselines']['B0']['C'] = 'b' * 64 + self.assertEqual(check.preparation_key(a), check.preparation_key(b)) + b['baselines']['B0']['cache']['bytes_sha256'] = 'c' * 64 + self.assertNotEqual(check.preparation_key(a), check.preparation_key(b)) + + def test_cross_pid_only_named_kernel_fields(self): + a, b = synthetic_report(), synthetic_report(2, 'diag-2') + b['baselines']['B1']['kernel']['_totals'] = {'path_query': 999} + b['baselines']['B1']['kernel']['_samples'] = {'path_query': [999]} + b['baselines']['B1']['K'] = 'b' * 64 + self.assertEqual(check.preparation_key(a), check.preparation_key(b)) + b['baselines']['B1']['kernel']['snapshot_version'] += 1 + self.assertNotEqual(check.preparation_key(a), check.preparation_key(b)) + + def test_fixture_pause_and_int64_identity(self): + report = synthetic_report() + report['baselines']['restored']['kernel']['paused'] = False + report['preparation'][1]['state']['kernel']['paused'] = False + report['baselines']['B0']['cache'].update(catalog_id='-9223372000000000001', + current_catalog_id='-9223372000000000001') + check.check_report(report) + self.reject(lambda r: r['baselines']['B0']['kernel'].update(paused=False)) + + def test_static_probe_and_product_mutations(self): + source = (check.HERE / 'diag_probe.gd').read_text() + check.check_probe(source) + mutations = [source.replace('clock.set_paused(true)', 'clock.set_paused(false)', 1), + source.replace('clock.advance_minutes(1)', 'clock.advance_minutes(2)'), + source + '\nfunc illegal():\n\tclock.advance_minutes(1)\n', + source + '\nfunc illegal():\n\truntime.initialize(clock, null)\n', + source + '\nfunc illegal():\n\truntime.spawn_guest()\n', + source + '\nfunc illegal():\n\truntime.set_script(null)\n', + source + '\nfunc illegal():\n\tclock.paused = false\n', + source + '\nfunc illegal():\n\truntime.economy_service.spend({})\n', + source + '\nfunc illegal():\n\tvar alias = runtime\n\talias.restore_snapshot({})\n', + source.replace('user://p28_diag/scratch/', 'user://saves/'), + source + '\nfunc illegal():\n\truntime.restore_snapshot({})\n', + source + '\nfunc illegal():\n\tSaveService.load_snapshot("user://x")\n'] + for mutation in mutations: + with self.subTest(mutation=mutation[-100:]), self.assertRaises(ValueError): + check.check_probe(mutation) + + def test_service_snapshot_inventory_matches_current_product(self): + source = (check.ROOT / 'autoload/simulation_runtime.gd').read_text() + body = source.split('func create_snapshot() -> Dictionary:', 1)[1].split('\nfunc restore_snapshot', 1)[0] + names = set(re.findall(r'\b(\w+)\.create_snapshot\(\)', body)) + self.assertEqual(names, set(check.SERVICES)) + + def test_source_map_count_and_mutations(self): + source = {'scripts/f%d.gd' % i: {'bytes': 1, 'sha256': H} for i in range(1407)} + check.check_source_maps(source, copy.deepcopy(source)) + for mutation in (lambda m: m.pop('scripts/f0.gd'), + lambda m: m['scripts/f0.gd'].update(sha256='b' * 64)): + after = copy.deepcopy(source) + mutation(after) + with self.assertRaises(ValueError): + check.check_source_maps(source, after) + + def test_import_closure_and_pins(self): + check.check_dependencies() + for name in check.LOCAL_NAMES - {'diag_probe.gd'}: + check.check_imports((check.HERE / name).read_text()) + for text in ('import unauthorized_project_module', 'from restart_pair import main', + 'eval("1")', '__import__("os")', + 'importlib.util.spec_from_file_location("x", "other.py")'): + with self.subTest(text=text), self.assertRaises(ValueError): + check.check_imports(text) + with tempfile.TemporaryDirectory() as tmp: + root = Path(tmp) + for name in check.DEPENDENCIES: + target = root / name + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes((check.ROOT / name).read_bytes()) + check.check_dependencies(root) + (root / next(iter(check.DEPENDENCIES))).write_text('changed') + with self.assertRaises(ValueError): + check.check_dependencies(root) + with self.assertRaises(ValueError): + check.check_modules({'bad': SimpleNamespace(__file__=str(check.ROOT / 'tools/unknown.py'))}) + + def test_headless_plan_and_stop_on_failure(self): + collector, _, _ = driver.load_dependencies() + commands = driver.plan(collector, Path('/engine'), Path('/output')) + self.assertEqual(len(commands), 6) + self.assertTrue(all('--headless' in c for c in commands)) + self.assertTrue(all('--check-only' in c for c in commands[1:3])) + rows = [] + iterator = driver.schedule(rows) + self.assertEqual(next(iterator), (0, 'import')) + rows.append({'phase': 'import', 'passed': False}) + with self.assertRaises(ValueError): + next(iterator) + + def test_bundle_missing_and_tampered(self): + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) + with self.assertRaisesRegex(ValueError, 'bundle missing'): + verify.verify(check.ROOT, path) + (path / 'raw.log').write_text('raw') + verify.seal(path) + (path / 'raw.log').write_text('tampered') + with self.assertRaisesRegex(ValueError, 'inventory/SHA'): + verify.verify(check.ROOT, path) + + def test_real_owned_python_child_and_raw_binding(self): + collector, _, outer = driver.load_dependencies() + child_text = ( + 'import os,sys,json,time,pathlib\n' + 'p=pathlib.Path(sys.argv[0]).parent\n' + 'identity={"pid":os.getpid(),"ppid":os.getppid(),"pgid":os.getpgrp(),' + '"args":" ".join([sys.executable]+sys.argv)}\n' + 'q=p/"identity.tmp"; q.write_text(json.dumps(identity)); q.replace(p/"identity")\n' + 'pathlib.Path(sys.argv[sys.argv.index("--log-file")+1]).write_text("native raw\\n")\n' + 'print("synthetic Python child; no Godot",flush=True)\n' + 'print("Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org",flush=True)\n' + 'time.sleep(.15)\n') + with tempfile.TemporaryDirectory() as tmp: + base = Path(tmp) + script = base / 'child.py' + script.write_text(child_text) + def diagnostic(command, folder, timeout, env, **kwargs): + def probe(pid): + deadline = time.monotonic() + 2 + while time.monotonic() < deadline: + if (base / 'identity').exists(): + identity = json.loads((base / 'identity').read_text()) + if identity['pid'] == pid: + return identity + time.sleep(.005) + raise RuntimeError('identity unavailable') + return outer.native.run_owned_diagnostic(command, folder, timeout, env, probe_fn=probe, **kwargs) + def run_phase(command, folder, timeout, env, **kwargs): + return outer.run_phase(command, folder, 5, env, diagnostic_fn=diagnostic, **kwargs) + with patch.object(collector, 'live_engines', return_value=[]): + row = collector.run_engine(SimpleNamespace(run_phase=run_phase), [sys.executable, str(script)], + base / 'import', {}) + row['phase'] = 'import' + self.assertTrue(row['passed'], row) + check.bound_raw(base / 'import', row) + bad = copy.deepcopy(row) + bad['capture']['actual_captured_pids'] = [row['process']['pid'] + 1] + with self.assertRaises(ValueError): + check.bound_raw(base / 'import', bad) + (base / 'import/stdout.log').write_text('changed') + with self.assertRaises(ValueError): + check.bound_raw(base / 'import', row) + cleanup, errors = outer.finish_captured(outer.OWNED) + self.assertEqual(errors, []) + self.assertTrue(all(r['engine_ended'] for r in cleanup)) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/verify_diag.py b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/verify_diag.py new file mode 100644 index 0000000..b586890 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p28-02/diag/verify_diag.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Join Root's actual native bundle; absent or synthetic evidence never passes.""" +import argparse +import json +import os +from pathlib import Path +import sys + +sys.dont_write_bytecode = True +import check_diag as check + + +def inventory(directory): + result = {} + for path in sorted(Path(directory).rglob('*')): + check.require(not path.is_symlink(), 'bundle symlink') + relative = path.relative_to(directory).as_posix() + # The stage is retained by Root; its full inventories are in execution.json. + # Bundle files include every raw/owned receipt, every script and admission. + if relative == 'bundle.json' or relative.startswith('stage/'): + continue + if path.is_file(): + result[relative] = check.pin(path) + return result + + +def seal(directory): + """Called by the Root driver only after owned cleanup and checker completion.""" + manifest = {'schema': 'p28-02-diag-bundle-v1', 'files': inventory(directory)} + with (Path(directory) / 'bundle.json').open('x') as stream: + json.dump(manifest, stream, indent=2, sort_keys=True) + + +def verify(project_root, directory): + project_root, directory = Path(project_root).resolve(), Path(directory).resolve() + check.require(directory.is_dir() and (directory / 'bundle.json').is_file(), 'actual native bundle missing') + manifest = json.loads((directory / 'bundle.json').read_text()) + check.require(manifest['schema'] == 'p28-02-diag-bundle-v1' and manifest['files'] == inventory(directory), + 'bundle inventory/SHA mismatch') + execution = json.loads((directory / 'execution.json').read_text()) + check.check_dependencies(project_root) + from diag_p28_02 import load_dependencies + collector, standard, _ = load_dependencies() + check.require(execution['source_before'] == execution['source_after'] and len(execution['source_before']) == 1407, + 'source map') + check.require(collector.source_map(standard, project_root) == execution['source_before'], + 'candidate source inventory differs from measured S') + for name, ref in execution['source_before'].items(): + path = project_root / name + check.require(path.resolve().is_relative_to(project_root) and check.pin(path) == ref, + 'candidate product source differs from measured S: ' + name) + local = project_root / check.HERE.relative_to(check.ROOT) + for name, ref in execution['worker_before'].items(): + check.require(name in check.LOCAL_NAMES and check.sha(local / name) == ref, + 'candidate diagnostic differs from measured S') + for name in check.LOCAL_NAMES - {'diag_probe.gd'}: + check.check_imports((local / name).read_text()) + check.check_modules() + return check.check_receipt(execution, directory) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--project-root', type=Path, required=True) + args = parser.parse_args() + try: + check.require(os.environ.get('BUILD_OUTPUT_DIR'), 'BUILD_OUTPUT_DIR required') + directory = Path(os.environ['BUILD_OUTPUT_DIR']) / 'p28-02-diag-evidence' + summary = verify(args.project_root, directory) + print(json.dumps({'status': 'verified', 'measured_source': 'S', 'candidate_Q_measured': False, + 'original_credit': 0, 'summary': summary}, ensure_ascii=False)) + return 0 + except (ValueError, KeyError, TypeError, OSError) as error: + print('FAIL: ' + str(error), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + raise SystemExit(main())