From 9c711860a82d597a48dab595a49d4f23c5f3c370 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Tue, 13 Oct 2026 00:57:32 +0900 Subject: [PATCH] =?UTF-8?q?P24-03:=20=E9=81=B8=E6=8A=9E=E3=81=A8=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E3=81=AE=E5=8F=97=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 11 ++ .../p24-03/ROOT_REVIEW_JSON_METADATA.json | 36 ++++ .../p24-03/evidence-index.pending.json | 16 ++ .../execution/p24-03/implementation.md | 52 +++++ scenes/main/main.gd | 9 +- scripts/ui/guest_observation_panel.gd | 5 +- tests/ui/test_selection_departure.gd | 96 ++++++++++ tests/ui/test_selection_departure.gd.uid | 1 + tools/check_p24_selection_evidence.py | 178 ++++++++++++++++++ tools/regression/suite-policy.json | 7 + 10 files changed, 408 insertions(+), 3 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_JSON_METADATA.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-03/evidence-index.pending.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p24-03/implementation.md create mode 100644 tests/ui/test_selection_departure.gd create mode 100644 tests/ui/test_selection_departure.gd.uid create mode 100644 tools/check_p24_selection_evidence.py diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 54a8438..28974df 100644 --- a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json +++ b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json @@ -2,6 +2,17 @@ "schema_version": 1, "scope": "exact per-suite completion label/schema; all check counts from actual raw only", "rows": [ + { + "test": "tests/ui/test_selection_departure.gd", + "template": "Selection departure: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "b43f3fa05e5ab0320e94e317ae75e880746d8557a69ea43bb6a40e93827a4eaf", + "authority": { + "path": "docs/spec/acceptance/P24-03-code-r02.json", + "scope": "new local suite source marker; actual execution pending" + } + }, { "test": "tests/business/test_business_closure.gd", "template": "Business closure: checks, failures", diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_JSON_METADATA.json b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_JSON_METADATA.json new file mode 100644 index 0000000..bee6c52 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_JSON_METADATA.json @@ -0,0 +1,36 @@ +{ + "scope": "JSON numeric metadata type diagnostic only; no game acceptance", + "actual_pid": 99508, + "exit_code": 0, + "command": [ + "/Applications/Godot.app/Contents/MacOS/Godot", + "--headless", + "--path", + "/private/tmp/the-tree-p24-json-c200043b18364206b2ca6f61179c4d62", + "--script", + "/private/tmp/the-tree-p24-json-c200043b18364206b2ca6f61179c4d62/probe.gd", + "--log-file", + "/private/tmp/the-tree-p24-json-c200043b18364206b2ca6f61179c4d62/engine.log" + ], + "raw": "Godot Engine v4.7.2.stable.official.ed1daf0bf - https://godotengine.org\n\n{\"engine_version\":{\"build\":\"official\",\"hash\":\"ed1daf0bf001b61586d9930840f2f1394092c079\",\"hex\":263938,\"major\":4,\"minor\":7,\"patch\":2,\"status\":\"stable\",\"string\":\"4.7.2-stable (official)\",\"timestamp\":0},\"is_float\":true,\"is_int\":false,\"profile\":\"/Users/korizatou114/Library/Application Support/TheTree-P24-JSON-5a698892c27c477f828a12f1841d8637\",\"version_value\":1.0}\n", + "probe_sha256": "2b6cba8ceee004d1788f69ba27cba73ebcb049dcce418baa9048400969cd5d34", + "finding": { + "severity": "high", + "file": "scenes/main/main.gd", + "functions": [ + "_restore_interface_selection", + "_restore_actor_interface_selection" + ], + "candidate_observed": "not selection.get(\"version\", 0) is int", + "impact": "SaveService._read_manifest JSON.parse_string produces 1.0 FLOAT, so normal version1 metadata will be silently rejected and valid selections lost.", + "required_original_behavior": "accept numeric exact1 from normal JSON while rejecting bool/string/non1 version; preserve old optional metadata fallback", + "acceptance_cases": [ + "normal saved JSON version1 decoded asfloat must restore map/staff/guest selection", + "bool true version must not count asnumeric1", + "1.5/string1/wrongtype safely ignored", + "no changes to gameplay snapshot or SaveService format needed" + ] + }, + "read_only_review_report": true, + "root_product_source_edits": 0 +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-03/evidence-index.pending.json b/docs/plans/original-completion-2026-10-02/execution/p24-03/evidence-index.pending.json new file mode 100644 index 0000000..686d131 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/evidence-index.pending.json @@ -0,0 +1,16 @@ +{ + "schema_version": 1, + "source_map": {}, + "artifacts": {}, + "conditions": { + "facility_lifecycle": {"status": "pending", "source_map": {}, "artifacts": []}, + "guest_lifecycle": {"status": "pending", "source_map": {}, "artifacts": []}, + "staff_lifecycle": {"status": "pending", "source_map": {}, "artifacts": []}, + "layer_and_values": {"status": "pending", "source_map": {}, "artifacts": []}, + "invalid_and_failed_load": {"status": "pending", "source_map": {}, "artifacts": []}, + "normal_os_input": {"status": "pending", "source_map": {}, "artifacts": []}, + "final_validation_review": {"status": "pending", "source_map": {}, "artifacts": []} + }, + "formal_acceptance": false, + "native_Windows": "pending" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-03/implementation.md b/docs/plans/original-completion-2026-10-02/execution/p24-03/implementation.md new file mode 100644 index 0000000..7f2492f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/implementation.md @@ -0,0 +1,52 @@ +# P24-03 code-role 提出 + +要件・受入ID: P24-03。基準commit: 286730f88ce430a1cbe1501d40361188f832d235。 +固定仕様: `docs/spec/acceptance/P24-03-code-r02.json`。 +提出状態は submitted。元88成果の1件の完了はRunnerと独立レビューが確定する。 + +## 変更と再利用 + +- Mainの施設・客・職員metadataのversion判定に数値型を要求。Godot JSONが返す数値1.0は互換性のため許可し、bool/string/非整数値を拒否。 +- 客panelの空選択時にOptionButtonを明示的に未選択にする。既存在館一覧・退館状態表示・履歴分離を維持。 +- 新局所suiteは実GuestServiceのLEAVING→despawn、一般職員remove、給与履歴保持、Unique拒否、UI収集時の全Runtime digest不変性、metadata型境界を確認する。関数呼出しはnative局所検証でありOS証拠には数えない。 +- 新suiteだけにpolicyとstrict markerを追加。既存suiteのSHA・assert・cleanup・marker条件は変更しない。 +- レイヤーは既存GameState snapshot、施設表示/地図強調は既存Main、職員一覧は既存service report、正式保存/失敗読込は既存契約を再利用。経済・Clock・人物・旧保存係数の変更なし。 +- 新検証器は読取専用。限定suite機能、見た目、素材、Runner操作、外部worktree入力は追加しない。 + +## 条件matrix(全項目未実行) + +| condition ID | 根拠source / Runnerで必要な検証 | 状態 | +| --- | --- | --- | +| facility_lifecycle | test_selection_lifecycle + test_selection_departure、実撤去後ID/detail/highlight、別process有効施設復元 | pending | +| guest_lifecycle | test_actor_selection + test_selection_departure、退館中表示・実despawn・履歴保持、別process画面値 | pending | +| staff_lifecycle | test_actor_selection + test_selection_departure、一般職員remove・給与履歴・Unique拒否、別process画面値 | pending | +| layer_and_values | test_selection_autosave + test_actor_selection + 新suite、正式GameState layer、UI収集時全Runtime不変、別process通常Main | pending | +| invalid_and_failed_load | test_selection_lifecycle + test_actor_selection + 新suite、不在/不正型/過大ID/旧metadata/読込失敗時保持 | pending | +| normal_os_input | 同候補sourceの通常Mainで施設/客/職員/レイヤー選択、正式保存、別PID再開、OS collector trace + PNG + log | pending | +| final_validation_review | 最終候補全roster/import/全script parse、正式独立Claudeレビュー、別Windows条件 | pending | + +## Runner実行計画 + +作成者は試験を実行していない。局所4suiteを確認した後、影響範囲(guest observation、staff management、layer skin、save/recovery)と最終候補の全回帰を正式実行する。全回帰には既存 `tools/run_all_tests.py` を指定なしで使用し、新suiteを含む全rosterとparse範囲を維持する。新suiteのcontrol labelはsingleであり、Runnerが渡すcontrol.jsonへ既存ordinary cleanupを出力する。 + +通常OS確認はRunnerが所有する新規profileで行う。個人profileや外部PIDを操作しない。Producerとconsumerの通常Main・同じ保存を保持し、別PIDで復元を観察する。OS未実行やWindows未実行は提出を妨げないが受入済みにはしない。修正後は同候補sourceの証拠を再取得し、過去結果を転用しない。 + +## 証拠index protocol v1 + +Runnerが `BUILD_OUTPUT_DIR/p24-evidence/index.json` を作成する。検証は `python3 tools/check_p24_selection_evidence.py --verify-only`。検証器は証拠を生成せず、欠損/pending/候補違い/失敗を非0で返す。通過も `formal_acceptance=false`。 + +indexのキー: + +- `schema_version`: 1。 +- `source_map`: `tools/run_all_tests.py` のsource_refsに含まれる**全source**について相対repo path→raw bytes SHA256。各条件・cross-process観察・OS collector・レビューでも同じmapを保持する。 +- `artifacts`: artifact名→`{path,sha256}`。pathはp24-evidence内相対path。空ファイル・symlink・上位参照は禁止。Runnerは元の正式証拠を変更せずコピーして登録する。 +- `conditions`: 上表の全ID→`{status:"executed",source_map,artifacts:[artifact名,...]}`。未実行はpending。局所条件には検証器指定の実suite raw logを含める。 +- `native`: `{suites:["tests/ui/test_actor_selection.gd","tests/ui/test_selection_lifecycle.gd","tests/ui/test_selection_autosave.gd","tests/ui/test_selection_departure.gd"]}`。全回帰内の実行と照合する。 +- `full_regression`: `{input,ended,rows,logs}`。input/ended/rowsはrunner-input.json、runner-ended.json、all-tests.jsonのartifact名。logsは全rosterのsuite path→元の実raw logのartifact名。所有PID・source・run_id・全roster・marker・全phaseを突合する。 +- `cross_process`: `{producer,consumer,manifest,world}`。正式保存のmanifest.json/world.binと両観察JSONのartifact名。観察JSONは `{source_map,scene:"res://scenes/main/main.tscn",pid,manifest_sha256,world_sha256,log,observation}`。両processで同一save SHA・別PIDが必要。 +- `observation`: `{interface_selection,layer,facility,guest,staff}`。interface_selectionは正式manifestの値、layerはGameStateの値。各対象は `{id,model_values,screen_values}`。facilityにはhighlight_idも必要。model_values/screen_valuesは空でない同形辞書で、実model値と画面に実表示された値を独立に収集する。表示のname/金額/状態/need等を単なるID確認に置き換えない。 +- cross-process `log` は通常Mainの実行・観察を採取したcollector raw logのartifact名。`P24_PROCESS pid=<実PID> scene=res://scenes/main/main.tscn` および `P24_OBSERVATION ` を収録する。作成者はこのログを生成していない。Runner collectorが実processと実画面を確認して収録する必要がある。 +- `os_input`: OS collector JSONのartifact名。JSONは `{source_map,collector,events}`。collectorはmacOS-CGEventTapまたはWindows-RawInput。各eventは `{action,pid,monotonic_ns,native_type,injected:false,screenshot,raw_trace}`。actionはselect_facility/select_guest/select_staff/select_layer/save/resumeを全て含む。native_typeはmouseDown/keyDown。PNGとcollectorの生trace(eventのsort_keys=True,ensure_ascii=False JSON行)を登録する。pressed.emit、関数呼出し、注入イベント、自己申告trueだけをOS証拠にしない。 +- `independent_review`: 最終別セッションClaudeレビューJSONのartifact名。`{source_map,reviewer:"Claude",independent_session,verdict:"pass",condition_ids:[全ID]}`。Runnerは実レビューと突合して保存する。コード作者の自己承認で代用しない。 + +このprotocolは証拠の整合性を検査する。artifactが実観察/正式レビュー由来であることの責任と元成果の判断はRunnerにあり、任意JSONやログを捏造して正式証拠に扱うことはできない。Windows結果は別に保存し、Mac結果から推測しない。 diff --git a/scenes/main/main.gd b/scenes/main/main.gd index eaa051f..5e0252d 100644 --- a/scenes/main/main.gd +++ b/scenes/main/main.gd @@ -2354,13 +2354,18 @@ func _apply_loaded_interface(result: Dictionary) -> void: _update_pause_and_speed() +func _valid_interface_selection_version(value: Variant) -> bool: + # JSON numbers may be decoded as floats; booleans are never versions. + return typeof(value) in [TYPE_INT, TYPE_FLOAT] and value == 1 + + func _restore_interface_selection(manifest: Variant) -> void: # Presentation metadata is optional. It must never invalidate gameplay, # and recovery must use the manifest of the actually loaded candidate. if not manifest is Dictionary: return var selection: Variant = manifest.get("interface_selection", {}) - if not selection is Dictionary or selection.get("version", 0) != 1: + if not selection is Dictionary or not _valid_interface_selection_version(selection.get("version", 0)): return var kind_value: Variant = selection.get("kind", "") var id_value: Variant = selection.get("id", "") @@ -2403,7 +2408,7 @@ func _restore_actor_interface_selection(manifest: Variant) -> void: if not manifest is Dictionary: return var selection: Variant = manifest.get("interface_selection", {}) - if not selection is Dictionary or selection.get("version", 0) != 1: + if not selection is Dictionary or not _valid_interface_selection_version(selection.get("version", 0)): return var staff_id: Variant = selection.get("staff_id", "") if staff_id is String and not staff_id.is_empty() and staff_id.length() <= 256 and _service(&"staff_service").has_staff(StringName(staff_id)): diff --git a/scripts/ui/guest_observation_panel.gd b/scripts/ui/guest_observation_panel.gd index 3955790..d4bf777 100644 --- a/scripts/ui/guest_observation_panel.gd +++ b/scripts/ui/guest_observation_panel.gd @@ -90,7 +90,10 @@ func refresh(select_first_if_empty: bool = true) -> void: if not _ids.has(selected_guest_id) and (not selected_guest_id.is_empty() or select_first_if_empty): selected_guest_id = _ids[0] if not _ids.is_empty() else &"" guests.disabled = _ids.is_empty() - if not _ids.is_empty(): guests.select(_ids.find(selected_guest_id)) + if _ids.has(selected_guest_id): + guests.select(_ids.find(selected_guest_id)) + else: + guests.select(-1) var report: Dictionary = service.get_observation(selected_guest_id) for need: StringName in bars: bars[need].value = report.get(&"needs", {}).get(need, 0) diff --git a/tests/ui/test_selection_departure.gd b/tests/ui/test_selection_departure.gd new file mode 100644 index 0000000..49e183e --- /dev/null +++ b/tests/ui/test_selection_departure.gd @@ -0,0 +1,96 @@ +extends SceneTree + +const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd") +var _checks := 0 +var _failures := 0 + +func _initialize() -> void: + call_deferred("_run") + +func _expect(ok: bool, message: String) -> void: + _checks += 1 + if not ok: + _failures += 1 + push_error(message) + +func _run() -> void: + var runtime: Variant = root.get_node("SimulationRuntime") + var clock: Variant = root.get_node("GameClock") + var state: Variant = root.get_node("GameState") + clock.reset(1, 6, 0) + state.reset() + runtime.initialize(clock, state, 42533) + clock.set_paused(true) + root.size = Vector2i(1440, 810) + var main: Control = load("res://scenes/main/main.tscn").instantiate() + root.add_child(main) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + await process_frame + await process_frame + var guest: GuestObservationPanel = main.call("_ensure_guest_observer") + var definition: GuestDefinition = runtime.guest_service.get_definitions()[0] + _expect(runtime.submit_command(&"spawn_guest", {&"guest_id": &"departure_guest", &"guest_type_id": definition.guest_type_id, &"gold": 731, &"options": {&"planned_exit_at": clock.absolute_minute + 1000}}).is_success(), "spawn actual guest") + guest.selected_guest_id = &"departure_guest" + guest.refresh() + _expect("731G" in guest.details.text, "selected guest displays authoritative wallet") + _expect(runtime.guest_service.begin_leaving(&"departure_guest", clock.absolute_minute + 1, &"TEST_EXIT").get(&"ok", false), "actual leaving transition") + guest.refresh() + _expect(guest.selected_guest_id == &"departure_guest" and "退館中" in guest.details.text, "leaving guest remains present and displays current state") + _expect(runtime.guest_service.despawn_guest(&"departure_guest", clock.absolute_minute + 2).get(&"ok", false), "actual guest despawn") + guest.refresh() + _expect(not runtime.guest_service.has_guest(&"departure_guest") and not runtime.guest_service.get_persistent_record(&"departure_guest").is_empty(), "history survives without a currently selectable guest") + _expect(guest.selected_guest_id.is_empty() and guest.guests.selected == -1 and guest.guests.disabled, "departed guest is absent from selection and dropdown") + _expect(guest.priority_label.text.is_empty() and guest.decision_label.text.is_empty(), "departed guest leaves no stale current detail values") + for need: StringName in guest.bars: + _expect(guest.bars[need].value == 0, "departed guest clears current need " + String(need)) + var staff: StaffManagementPanel = main.get("staff_management_panel") + _expect(runtime.staff_service.create_staff(&"departure_staff", &"cleaner", {&"unpaid_wages": 43}).is_success(), "register ordinary departure employee") + staff.select_staff(&"departure_staff") + _expect(staff.selected_staff_id == &"departure_staff", "select ordinary employee") + _expect(runtime.staff_service.remove_staff(&"departure_staff").is_success(), "permitted ordinary employee removal") + staff.refresh() + _expect(not runtime.staff_service.has_staff(&"departure_staff") and not staff.roster_buttons.has(&"departure_staff") and staff.selected_staff_id != &"departure_staff", "removed employee no longer selectable") + _expect(runtime.staff_service.get_departed_unpaid_wages(&"departure_staff") == 43, "departure preserves payroll history") + _expect(runtime.staff_service.create_staff(&"departure_unique", &"cleaner", {&"employment_kind": StaffRuntime.EmploymentKind.UNIQUE}).is_success(), "register protected Unique fixture") + staff.select_staff(&"departure_unique") + _expect(not runtime.staff_service.remove_staff(&"departure_unique").is_success(), "Unique removal remains forbidden") + staff.refresh() + _expect(runtime.staff_service.has_staff(&"departure_unique") and staff.selected_staff_id == &"departure_unique", "rejected removal preserves Unique selection") + main.call("_set_layer", 1) + var before: Dictionary = runtime.create_snapshot() + var selection: Dictionary = main.call("_capture_interface_selection") + _expect(SimulationDigest.compute(before) == SimulationDigest.compute(runtime.create_snapshot()), "UI metadata capture leaves whole Runtime unchanged") + _expect(state.create_snapshot().get(&"active_layer") == 1 and selection.get(&"staff_id") == "departure_unique", "formal layer and selected ID have authoritative sources") + guest.clear_selection() + _expect(guest.guests.selected == -1, "clear observer has no invalid option index") + for version: Variant in [true, 1.5, "1", 0, 2]: + main.call("_restore_actor_interface_selection", {"interface_selection": {"version": version, "staff_id": "departure_unique", "staff_open": true}}) + _expect(staff.selected_staff_id.is_empty() and not staff.visible, "wrong/old metadata version is ignored: " + str(version)) + _expect(runtime.submit_command(&"build_facility", {&"definition_id": &"guest_corridor", &"origin": Vector3i(4, 1, 0)}).is_success(), "real facility construction starts") + clock.advance_minutes(100) + var facilities: Array = runtime.construction_service.get_facilities() + _expect(not facilities.is_empty(), "real facility construction completes") + if not facilities.is_empty(): + var facility_id := StringName(facilities[0][&"instance_id"]) + main.call("_on_facility_selected", facility_id) + main.get("world_preview").set("selected_entity_id", facility_id) + var previous_detail: String = main.get("selected_detail_label").text + _expect(main.get("_selected_entity_id") == facility_id, "select existing built facility") + _expect(runtime.submit_command(&"demolish_facility", {&"instance_id": facility_id}).is_success(), "real facility demolition commits") + main.call("_refresh_selected_entity") + _expect(main.get("_selected_entity_id") == &"" and main.get("_selected_entity_kind") == &"" and main.get("world_preview").get("selected_entity_id") == &"", "actual demolition clears ID kind and map highlight") + _expect(main.get("selected_entity_icon").texture == null and main.get("selected_detail_label").text != previous_detail, "actual demolition clears icon and stale detail") + var original: String = OS.get_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF") + if original.is_empty() or not original.is_absolute_path(): + _expect(false, "Runner must provide fresh absolute ordinary control proof") + else: + var control := original + if FileAccess.file_exists(control): + _expect(false, "ordinary cleanup proof overwrite forbidden") + else: + OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", control) + var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main) + OS.set_environment("THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF", original) + _expect(bool(cleanup.get(&"passed", false)), "ordinary Main/audio cleanup") + print("Selection departure: %d checks, %d failures" % [_checks, _failures]) + quit(1 if _failures else 0) diff --git a/tests/ui/test_selection_departure.gd.uid b/tests/ui/test_selection_departure.gd.uid new file mode 100644 index 0000000..6926445 --- /dev/null +++ b/tests/ui/test_selection_departure.gd.uid @@ -0,0 +1 @@ +uid://c3p24departur diff --git a/tools/check_p24_selection_evidence.py b/tools/check_p24_selection_evidence.py new file mode 100644 index 0000000..2f765e9 --- /dev/null +++ b/tools/check_p24_selection_evidence.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Read-only P24 evidence gate. Success is machine validation, never acceptance.""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import sys + +sys.dont_write_bytecode = True +ROOT = Path(__file__).resolve().parents[1] +CONDITIONS = ('facility_lifecycle', 'guest_lifecycle', 'staff_lifecycle', + 'layer_and_values', 'invalid_and_failed_load', 'normal_os_input', + 'final_validation_review') + + +def require(ok, message): + if not ok: + raise ValueError(message) + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def read(path): + return json.loads(path.read_bytes()) + + +def load_module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def verify(base): + specification = ROOT / 'docs/spec/acceptance/P24-03-code-r02.json' + require(hashlib.sha256(specification.read_text().strip().encode('utf-8')).hexdigest() == 'dbb885b47a3ee4af3d4180bb97fe746e26a0e08d75af2cd3b5b727ed337bd37c', 'fixed P24 specification mismatch') + index = read(base / 'index.json') + require(index.get('schema_version') == 1, 'index schema_version must be 1') + runner = load_module('p24_runner_source', ROOT / 'tools/run_all_tests.py') + current = {name: pin['sha256'] for name, pin in runner.source_refs(ROOT).items()} + require(index.get('source_map') == current, 'index source_map differs from current whole candidate') + artifacts = index.get('artifacts', {}) + require(isinstance(artifacts, dict) and artifacts, 'missing artifacts') + files = {} + for name, pin in artifacts.items(): + relative = Path(pin['path']) + require(not relative.is_absolute() and '..' not in relative.parts, 'artifact path must stay inside evidence') + path = base / relative + require(not any(p.is_symlink() for p in [path, *path.parents]), 'artifact symlink refused') + require(path.is_file() and path.stat().st_size > 0 and sha(path) == pin['sha256'], 'missing/changed artifact: ' + name) + files[name] = path + + def artifact(name): + require(name in files, 'unregistered artifact: ' + str(name)) + return files[name] + + matrix = index.get('conditions', {}) + require(set(matrix) == set(CONDITIONS), 'condition IDs missing=%s unknown=%s' % (sorted(set(CONDITIONS) - set(matrix)), sorted(set(matrix) - set(CONDITIONS)))) + for name in CONDITIONS: + row = matrix[name] + require(row.get('source_map') == current, 'condition candidate mismatch: ' + name) + require(row.get('status') == 'executed' and row.get('artifacts'), 'pending condition: ' + name) + for reference in row['artifacts']: + artifact(reference) + + # Corroborate the full roster against existing strict runner receipts and raw logs. + regression = index['full_regression'] + native = index['native'] + expected_local = ['tests/ui/test_actor_selection.gd', 'tests/ui/test_selection_lifecycle.gd', + 'tests/ui/test_selection_autosave.gd', 'tests/ui/test_selection_departure.gd'] + require(native.get('suites') == expected_local, 'native selection suite coverage missing') + start = read(artifact(regression['input'])) + end = read(artifact(regression['ended'])) + rows_path = artifact(regression['rows']) + rows = read(rows_path) + roster = runner.roster(ROOT) + require(start.get('selected_source_map') == current and start.get('roster') == roster, 'regression source/whole roster mismatch') + require(start.get('protocol') == 'guarded-standard-v1' and end.get('protocol') == start['protocol'], 'regression protocol') + require(start.get('run_id') and end.get('run_id') == start['run_id'] and end.get('owner_runner_pid') == start.get('owner_runner_pid'), 'regression ownership mismatch') + require(end.get('passed') is True and end.get('engine_ended') is True and not end.get('error') and not end.get('cleanup_errors') and not end.get('guard_errors'), 'regression incomplete/failed') + require(end.get('all_tests_sha256') == sha(rows_path) and [r['test'] for r in rows] == roster, 'regression rows checksum/roster mismatch') + markers = load_module('p24_strict_markers', ROOT / 'tools/strict_test_markers.py') + contracts = markers.load_contracts(ROOT / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json') + require(start.get('marker_contract_sha256') == sha(ROOT / 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'), 'marker contract mismatch') + for row in rows: + require(type(row.get('actual_pid')) is int and row['actual_pid'] > 0 and row['actual_pid'] in end['actual_captured_PIDs'], 'native PID mismatch') + require(row.get('passed') is True and row.get('exit_code') == 0 and row.get('engine_ended') is True, 'native suite failed/unexecuted') + require(row.get('source_sha256') == current[row['test']] and row.get('source_sha256_end') == current[row['test']], 'native suite source mismatch') + log = artifact(regression['logs'][row['test']]) + require(sha(log) == row['log_sha256'], 'native raw log checksum mismatch') + parsed = markers.parse_raw(row['test'], log.read_text(), contracts, current[row['test']]) + require(parsed['passed'] and parsed['checks'] == row['reported_checks'], 'native strict marker failed') + required_logs = { + 'facility_lifecycle': ['tests/ui/test_selection_lifecycle.gd', 'tests/ui/test_selection_departure.gd'], + 'guest_lifecycle': ['tests/ui/test_actor_selection.gd', 'tests/ui/test_selection_departure.gd'], + 'staff_lifecycle': ['tests/ui/test_actor_selection.gd', 'tests/ui/test_selection_departure.gd'], + 'layer_and_values': ['tests/ui/test_selection_autosave.gd', 'tests/ui/test_actor_selection.gd'], + 'invalid_and_failed_load': ['tests/ui/test_selection_lifecycle.gd', 'tests/ui/test_actor_selection.gd'], + } + for condition, suites in required_logs.items(): + require(set(matrix[condition]['artifacts']) >= {regression['logs'][s] for s in suites}, 'condition lacks actual native logs: ' + condition) + phases = end.get('phases', []) + require(len(phases) == len(roster) + 2 and all(p.get('passed') is True for p in phases), 'import/whole parse/whole regression incomplete') + + # A producer and consumer must observe one unmodified formal save package. + cross = index['cross_process'] + for condition in ('facility_lifecycle', 'guest_lifecycle', 'staff_lifecycle', 'layer_and_values'): + require(set(matrix[condition]['artifacts']) >= {cross[k] for k in ('producer', 'consumer', 'manifest', 'world')}, 'condition lacks cross-process/save artifacts: ' + condition) + observations = [read(artifact(cross[k])) for k in ('producer', 'consumer')] + manifest_path = artifact(cross['manifest']) + world = artifact(cross['world']) + manifest = read(manifest_path) + data = world.read_bytes() + require(data[:4] == b'TTWB' and len(data) > 16 and manifest['checksum_world'] == hashlib.sha256(data[16:]).hexdigest(), 'formal world header/checksum mismatch') + require(int.from_bytes(data[4:8], 'little') == manifest['format_version'] and int.from_bytes(data[8:16], 'little') == manifest['uncompressed_size'], 'formal world version/size mismatch') + for report in observations: + require(report.get('source_map') == current and report.get('scene') == 'res://scenes/main/main.tscn', 'cross-process normal Main/source mismatch') + require(type(report.get('pid')) is int and report['pid'] > 0, 'cross-process PID missing') + require(report.get('manifest_sha256') == sha(manifest_path) and report.get('world_sha256') == sha(world), 'cross-process save mismatch') + log = artifact(report['log']) + require(('P24_PROCESS pid=%d scene=res://scenes/main/main.tscn' % report['pid']) in log.read_text(), 'cross-process PID/scene absent from raw log') + require(('P24_OBSERVATION ' + json.dumps(report['observation'], sort_keys=True, ensure_ascii=False)) in log.read_text(), 'cross-process observation absent from raw log') + require(report['observation']['interface_selection'] == manifest['interface_selection'], 'formal saved selection mismatch') + require(observations[0]['pid'] != observations[1]['pid'], 'restart must use distinct processes') + require(observations[0]['observation'] == observations[1]['observation'], 'restored selections/values/layer differ') + observed = observations[0]['observation'] + require(set(observed) >= {'interface_selection', 'layer', 'facility', 'guest', 'staff'}, 'cross-process value coverage missing') + require(type(observed['layer']) is int and observed['layer'] in (0, 1), 'cross-process layer invalid') + for kind in ('facility', 'guest', 'staff'): + value = observed[kind] + require(isinstance(value, dict) and value.get('id') and value.get('model_values') and value.get('screen_values') == value.get('model_values'), 'model/display values missing or inconsistent: ' + kind) + key = 'id' if kind == 'facility' else kind + '_id' + require(observed['interface_selection'].get(key) == value['id'], 'observed/saved ID mismatch: ' + kind) + require(observed['facility'].get('highlight_id') == observed['facility']['id'], 'restored map highlight mismatch') + + # OS collector events must identify actual input, target process and captured frames. + os_report = read(artifact(index['os_input'])) + require(index['os_input'] in matrix['normal_os_input']['artifacts'], 'OS condition lacks collector artifact') + require(os_report.get('source_map') == current and os_report.get('collector') in ('macOS-CGEventTap', 'Windows-RawInput'), 'OS collector/source mismatch') + events = os_report.get('events', []) + required_actions = {'select_facility', 'select_guest', 'select_staff', 'select_layer', 'save', 'resume'} + require({e.get('action') for e in events} >= required_actions, 'OS action coverage missing') + for event in events: + require(event.get('pid') in [r['pid'] for r in observations] and type(event.get('monotonic_ns')) is int, 'OS target/time mismatch') + require(event.get('native_type') in ('mouseDown', 'keyDown') and event.get('injected') is False, 'function/emit/synthetic event is not OS input') + frame = artifact(event['screenshot']) + require(frame.read_bytes().startswith(b'\x89PNG\r\n\x1a\n'), 'OS screenshot missing/invalid') + trace = artifact(event['raw_trace']).read_text() + require(json.dumps(event, sort_keys=True, ensure_ascii=False) in trace, 'OS event missing in collector trace') + + review = read(artifact(index['independent_review'])) + require(set(matrix['final_validation_review']['artifacts']) >= {regression['input'], regression['ended'], regression['rows'], index['independent_review']}, 'final condition lacks full regression/review artifacts') + require(review.get('source_map') == current and review.get('reviewer') == 'Claude' and review.get('independent_session') and review.get('verdict') == 'pass', 'independent final Claude review missing') + require(set(review.get('condition_ids', [])) == set(CONDITIONS), 'independent review coverage missing') + return {'machine_validation': True, 'formal_acceptance': False, 'conditions': list(CONDITIONS)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--verify-only', action='store_true', required=True) + args = parser.parse_args() + build = os.environ.get('BUILD_OUTPUT_DIR') + try: + require(bool(build), 'BUILD_OUTPUT_DIR is required') + result = verify(Path(build).resolve() / 'p24-evidence') + print(json.dumps(result, ensure_ascii=False)) + return 0 + except (OSError, ValueError, KeyError, TypeError, AttributeError, IndexError, AssertionError) as error: + print(json.dumps({'machine_validation': False, 'formal_acceptance': False, 'error': str(error)}, ensure_ascii=False)) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/regression/suite-policy.json b/tools/regression/suite-policy.json index 3969e97..0700874 100644 --- a/tools/regression/suite-policy.json +++ b/tools/regression/suite-policy.json @@ -1,6 +1,13 @@ { "schema_version": 1, "suite_policies": { + "tests/ui/test_selection_departure.gd": { + "source_sha256": "b43f3fa05e5ab0320e94e317ae75e880746d8557a69ea43bb6a40e93827a4eaf", + "ordinary_cleanup_prepared": true, + "ordinary_control_labels": ["single"], + "old_bounded_teardown": false, + "new_audio_QA": false + }, "tests/ui/test_anri_recruitment.gd": { "source_sha256": "74cfaf73ed01769c49498e5395fda009b70f2f6667f4f902abf4697f01f87962", "ordinary_cleanup_prepared": true,