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..9438e65 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": "3137f36f547514f25ce6ac775fa7a5141d12f83ca4b95da0be45fecfafe9d9b4", + "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_EVIDENCE_GATE.json b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_EVIDENCE_GATE.json new file mode 100644 index 0000000..526645a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_EVIDENCE_GATE.json @@ -0,0 +1,27 @@ +{ + "read_only_review_report": true, + "root_product_source_edits": 0, + "findings": [ + { + "severity": "high", + "file": "tools/check_p24_selection_evidence.py", + "issue": "gate requires independent_review verdict pass before Runner.validate, while Runner.review requires validation pass. This creates a cycle or an unnecessary second review.", + "required_behavior": "Machine validation must validate actual native/cross-PID/OS/full-regression evidence; Runner formal independent review is the downstream completion gate. Keep original final_validation_review requirement and original-unit incomplete until formal review, but do not fabricate review or require a prior duplicate review to execute validation." + }, + { + "severity": "medium", + "file": "tools/check_p24_selection_evidence.py", + "issue": "collector limited to CGEventTap/Windows-RawInput and injected=false adds a hardware-only condition absent from original acceptance. CUA normal OS delivered input is synthesized at OS level, distinct from Godot pressed.emit/Input.parse_input_event. Requirement forbids treating function/emit alone as OS evidence, not all native OS automation.", + "required_behavior": "Permit actual CUA native app calls with faithful step records and observed screens, preferably corroborated by passive Godot _input/GUI input receipts and exact candidate/PID/save/source. Clearly disclose tool-generated OS events and trace limitations. Do not require new InputMonitoring permission or physical hardware only. Direct Godot function/emit or synthetic engine-only fixture must remain insufficient." + } + ], + "reused_method_context": "Existing P05-01 accepted finite OS evidence explicitly used root actual CUA calls and returned screens, with the stated limitation that external trace was faithful transcription rather than independently instrumented original tool receipt. This is a method reference only, not a reused current pass.", + "keep_conditions": [ + "all 7 condition IDs and no partial original completion", + "exact whole candidate source map", + "real producer/consumer distinct processes and immutable formal package", + "strict full regression and exact raw evidence", + "independent Claude formal review after validation" + ], + "implementation_scope": "Comments for implementation owner only; root has not changed gate code or worker source." +} 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/ROOT_REVIEW_LOCAL_FAILURE.json b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_LOCAL_FAILURE.json new file mode 100644 index 0000000..001f9f9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_LOCAL_FAILURE.json @@ -0,0 +1,21 @@ +{ + "read_only_review_report": true, + "candidate": "9c711860a82d597a48dab595a49d4f23c5f3c370", + "evidence_root": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p24-03-local-native-r01", + "import_parse": "pass; 304 scripts", + "local_suite": "test_selection_departure.gd failed, 31 checks, four errors", + "failures": [ + "departed guest is absent from selection and dropdown (line42)", + "departed guest leaves no stale current detail values (line43)", + "real facility construction starts (line69)", + "real facility construction completes (line72)" + ], + "repair_instructions": [ + "Inspect actual guest roster and refresh fallback. Valid other guests can remain and may be selected; do not change correct normal first-row behavior merely to satisfy an incorrect empty-roster fixture. Prove departed ID not referenced; create genuine empty-roster case separately if needed.", + "Use valid real construction coordinates/unlock/payment prerequisites based on existing lifecycle test, record real command result, then prove actual demolition and cleared inspector. Do not bypass services or weaken acceptance.", + "Read ROOT_REVIEW_EVIDENCE_GATE.json and resolve validation/review cycle and over-restrictive OS collector.", + "Implement distinct-process save/restart passive fixture/protocol within allowed paths if needed to enable real remaining acceptance, not fake evidence.", + "Coalesce related changes, update strict source marker SHA/contracts, avoid full regression until local pass. Root will run owned guarded native phases." + ], + "root_product_source_edits": 0 +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_SECOND_LOCAL.json b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_SECOND_LOCAL.json new file mode 100644 index 0000000..574046f --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/ROOT_REVIEW_SECOND_LOCAL.json @@ -0,0 +1,10 @@ +{ + "read_only_review_report": true, + "candidate": "c2b8772e237597930e67f1329d8b61143614a244", + "local_result": "import and 304 parse passed; departure37checks2failures; guards and cleanup pass; other3notrun", + "evidence_root": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p24-03-local-native-r02", + "failure": "construction start/complete; message prints RefCounted identity, not actual CommandResult", + "diagnosis": "Possible fixture ordering interference: test creates then removes ordinary employee with43 unpaid wages, deliberately preserving departed debt. Runtime _operations_construction_investment_gate lines7988-8001 correctly rejects construction if _operations_total_arrears()>0. Build/demolish should precede unpaid-wage departure boundary or use independently fresh runtime/Main via ordinary cleanup. Do not bypass legitimate debt gate. Print build_result.to_dictionary for actionable true error. Verify this rather than merely changing coordinates again.", + "required_remaining_implementation": "Add allowed tests/ui/fixtures/selection_cross_process.gd passive, separate-process normal-Main collector; lacking independently read actual model fields/screen widgets prevents root preparing truthful exact-current cross-process and OS proof. Existing Main has no external introspection endpoint. Fixture should instantiate ordinary Main, prepare boundary target via actual commands for producer if needed, pause clock for unchanged observations, expose actualPID/model/screen/selected layer/highlights via JSON and rawlog plus frames; source map supplied by rootcontroller; save/load and selection for OS proof MUST be actual native CUA user-facing controls, not injected Godot engine events. Consumer uses same immutable formalpackage in distinct process/private-profile, source/project guards stay unchanged. No manufactured accepted index or reviewer. Provide documented env/control/paths, passive poll output and ordinary owned teardown. No testunit denominators added.", + "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..5be9cdb --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p24-03/implementation.md @@ -0,0 +1,75 @@ +# 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証拠には数えない。 +- ROOT_REVIEW_LOCAL_FAILUREを反映し、実離脱後は離脱IDの除外と在館客への正常fallbackを確認。残る在館客をserviceの正式離脱で除去した別境界で空一覧・表示値消去を確認する。建設は既存lifecycle suiteの有効な受付座標(-8,0,0)と120分完成を再利用し、実command結果を失敗messageへ記録する。 +- 新suiteだけのraw SHAをpolicy/markerへ更新。理由は上記fixture不備の修正であり、既存suiteのSHAやmarkerは変更しない。正式Runner再実行はpending。 +- 新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を含める。`final_validation_review`だけは全回帰実行済み・正式レビュー待ちを`pending_review`で表す。全回帰input/ended/rowsを必須とし、machine validation後にRunnerが正式独立レビューを実行する。元単位はレビュー終了まで未完了。 +- `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、またはCUA-native-app。各eventは `{action,pid,monotonic_ns,native_type,delivery:"native_os",injected,screenshot,raw_trace}`。actionはselect_facility/select_guest/select_staff/select_layer/save/resumeを全て含む。saveはproducer PID、resumeはconsumer PID。native_typeはmouseDown/keyDown。PNGとtrace(eventのsort_keys=True,ensure_ascii=False JSON行)を登録し、normal_os_input条件のartifactsにも含める。OS配信された自動操作は許可し、injectedで由来を開示する。pressed.emit、ゲーム関数呼出し、engine内部注入だけはOS証拠にならない。 +- CUA-native-appは`trace_provenance`(original_tool_receiptsまたはfaithful_transcription)と非空の`limitations`を報告し、各eventに`injected:true`、`tool_step` artifactを持つ。step JSONは`{pid,action,monotonic_ns,api:"native_app",operation:"click"または"key",arguments,screenshot,source_map}`。argumentsは実呼出し引数、screenshotは実返却画面。stepも条件artifactに含める。書き起こしの限界とtool生成OS入力を明記し、実CUA呼出し・観察に基づくことをRunnerが照合する。新InputMonitoring許可や物理入力のみという条件は課さない。 +- `independent_review`: 最終別セッションClaudeレビューJSONのartifact名。`{source_map,reviewer:"Claude",independent_session,verdict:"pass",condition_ids:[全ID]}`。machine validationより後段のRunner正式レビュー。pending_reviewでは未要求、executedへ更新する場合はこのartifactと全条件coverageを検証する。Runnerは実レビューと突合して保存する。コード作者の自己承認で代用しない。 + +このprotocolは証拠の整合性を検査する。artifactが実観察/正式レビュー由来であることの責任と元成果の判断はRunnerにあり、任意JSONやログを捏造して正式証拠に扱うことはできない。Windows結果は別に保存し、Mac結果から推測しない。 + +## 今回の提出で残る実行 + +別process fixtureとして `tests/ui/fixtures/selection_cross_process.gd` を追加した。通常Mainをinstantiateする受動観察器であり、選択・保存・読込・Clock変更・command・入力注入を行わない。局所suiteはOS入力の代用にしない。source_mapと正式manifest/worldのSHAを添えた観察、CUA stepと画面、全回帰、正式レビューはいずれも未取得でpending。作成者はgit書込み、Runner操作、試験実行を行っていない。 + +## SECOND_LOCAL記録に対する修正(正式再実行pending) + +実建設/撤去をguest/staff離脱境界より前へ移動した。意図的に43Gの離職給与債務を保持した後の建設は正規債務ガードに阻まれるためであり、経済ルールは変更しない。assertは維持し、建設失敗messageは `CommandResult.to_dictionary()` を表示する。変更した新suiteだけのSHAをpolicy/markerへ更新した。これは追加のfixture修正であり、既存suiteのSHA/marker/cleanupは変更しない。 + +### 受動別process collectorの実行契約 + +Runnerは同候補project、別PID、専用private user-data環境で `--script res://tests/ui/fixtures/selection_cross_process.gd` をGUI起動する。`P24_OBSERVER_OUTPUT` はそのprocess専用の既存絶対ディレクトリ、`P24_OBSERVER_SOURCE_MAP` は現候補のpath→raw SHA256 map JSONの絶対パス。別worktreeは入力にしない。入力とsource guardは既存Runner側で照合する。 + +通常Mainの画面操作で必要対象の準備・選択・pause・正式保存・別process読込を行う。observerは1秒間隔で変化した観察を、実PID/連番付きJSONとPNG、stdout `P24_PROCESS` / `P24_RAW_OBSERVATION` として保存し、既存artifactの上書きを拒否する。不要なprocessの終了は通常MainのOSウィンドウcloseに従い、終了強制や外部PID操作を追加しない。追加のtest suiteではなく、roster/成果分母を増やさない。 + +JSONの `raw_observation` はモデル側facility snapshot、guest observation、staff management reportを、実Labelのheading/detail/priority/decision・need bar・editor可視性・地図highlightとは独立に採取する。表示を再生成してscreen値と称しない。未選択/不存在は空modelとして残し、成功とは判定しない。PNGとJSONはraw証拠であり正式save SHAやOS traceを自己申告しない。 + +Runner collectorはこのraw証拠と実画面・正式packageを突合し、既存index protocolの `model_values` / `screen_values` を独立に抽出する。表示されていないモデル値をscreen側へコピーしてはならない。`P24_RAW_OBSERVATION` は既存検証器が要求する正規化済み `P24_OBSERVATION` と区別する。正式manifest/worldのSHA、元raw report/frame、OS入力traceへの由来を保持し、未取得はpending。collectorのparseと同一package別PID復元はRunnerの局所/影響範囲検証に含める。 + +読取専用の静的確認としてPython検証器のAST parseと、新suiteのraw SHAがpolicy/markerの両方と一致することを確認した。Godot parse、局所suite、証拠検証器そのものの実行結果は未取得であり、静的確認を正式試験の合格として扱わない。 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/fixtures/selection_cross_process.gd b/tests/ui/fixtures/selection_cross_process.gd new file mode 100644 index 0000000..9fa2b14 --- /dev/null +++ b/tests/ui/fixtures/selection_cross_process.gd @@ -0,0 +1,100 @@ +extends SceneTree + +## Passive observer: no selection, save/load, command or engine input injection. +## Runner launches separate owned processes with private user-data directories. +var _main: Control +var _elapsed := 0.0 +var _last := "" +var _sequence := 0 +var _output := "" +var _source_map: Dictionary = {} + +func _initialize() -> void: + call_deferred("_start") + +func _start() -> void: + _output = OS.get_environment("P24_OBSERVER_OUTPUT") + var pins := OS.get_environment("P24_OBSERVER_SOURCE_MAP") + if not _output.is_absolute_path() or not DirAccess.dir_exists_absolute(_output) or not pins.is_absolute_path(): + push_error("P24 observer requires existing absolute output directory and source-map JSON") + quit(1) + return + var parsed: Variant = JSON.parse_string(FileAccess.get_file_as_string(pins)) + if not parsed is Dictionary or parsed.is_empty(): + push_error("P24 observer source map missing") + quit(1) + return + _source_map = parsed + root.size = Vector2i(1440, 810) + _main = load("res://scenes/main/main.tscn").instantiate() + root.add_child(_main) + print("P24_PROCESS pid=%d scene=res://scenes/main/main.tscn" % OS.get_process_id()) + # Use the ordinary Main pause control through OS input when a frozen state + # is needed. The observer itself never changes Runtime or Clock. + +func _process(delta: float) -> bool: + _elapsed += delta + if _elapsed >= 1.0 and is_instance_valid(_main): + _elapsed = 0.0 + _capture() + return false + +func _capture() -> void: + var runtime: Variant = root.get_node("SimulationRuntime") + var selection: Dictionary = _main.call("_capture_interface_selection") + var facility_id := StringName(selection.get("id", "")) + var facility: Variant = runtime.facility_service.get_instance(facility_id) + var guest_panel: GuestObservationPanel + var dialog: Variant = _main.get("guest_observer") + if dialog != null: + guest_panel = dialog.find_child("GuestObservationPanel", true, false) as GuestObservationPanel + var staff_panel: StaffManagementPanel = _main.get("staff_management_panel") + var staff_model: Dictionary = {} + for row: Dictionary in runtime.get_staff_management_report().get(&"staff", []): + if String(row.get(&"staff_id", "")) == String(selection.get("staff_id", "")): + staff_model = row.duplicate(true) + var needs: Dictionary = {} + if guest_panel != null: + for need: StringName in guest_panel.bars: + needs[need] = guest_panel.bars[need].value + var observation := { + "interface_selection": selection, + "layer": root.get_node("GameState").get("active_layer"), + "facility": {"id": String(facility_id), "model_snapshot": facility.create_snapshot() if facility != null else {}, + "heading_text": _main.get("selected_heading_label").text, + "detail_text": _main.get("selected_detail_label").text, + "highlight_id": String(_main.get("world_preview").get("selected_entity_id"))}, + "guest": {"id": selection.get("guest_id", ""), + "model_report": runtime.guest_service.get_observation(StringName(selection.get("guest_id", ""))), + "detail_text": guest_panel.details.text if guest_panel != null else "", + "priority_text": guest_panel.priority_label.text if guest_panel != null else "", + "decision_text": guest_panel.decision_label.text if guest_panel != null else "", "need_bars": needs}, + "staff": {"id": selection.get("staff_id", ""), "model_report": staff_model, + "detail_text": staff_panel.staff_detail_label.text, + "editor_visible": staff_panel.editor_panel.is_visible_in_tree()}, + } + var encoded := JSON.stringify(observation, "", true) + if encoded == _last: + return + _sequence += 1 + var stem := "process-%d-observation-%d" % [OS.get_process_id(), _sequence] + var report_path := _output.path_join(stem + ".json") + var frame_path := _output.path_join(stem + ".png") + if FileAccess.file_exists(report_path) or FileAccess.file_exists(frame_path): + push_error("P24 observer refuses artifact overwrite") + return + var image := root.get_texture().get_image() + if image.save_png(frame_path) != OK: + push_error("P24 observer frame capture failed") + return + var file := FileAccess.open(report_path, FileAccess.WRITE) + if file == null: + push_error("P24 observer report write failed") + return + file.store_string(JSON.stringify({"pid": OS.get_process_id(), "source_map": _source_map, + "scene": "res://scenes/main/main.tscn", "ticks_usec": Time.get_ticks_usec(), + "raw_observation": observation, "screenshot": stem + ".png", + "formal_acceptance": false}, "\t", true) + "\n") + file.close() + _last = encoded + print("P24_RAW_OBSERVATION " + encoded) diff --git a/tests/ui/fixtures/selection_cross_process.gd.uid b/tests/ui/fixtures/selection_cross_process.gd.uid new file mode 100644 index 0000000..2df008a --- /dev/null +++ b/tests/ui/fixtures/selection_cross_process.gd.uid @@ -0,0 +1 @@ +uid://d24crossproc diff --git a/tests/ui/test_selection_departure.gd b/tests/ui/test_selection_departure.gd new file mode 100644 index 0000000..c50b11b --- /dev/null +++ b/tests/ui/test_selection_departure.gd @@ -0,0 +1,107 @@ +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 + # Build before intentionally preserving departed payroll debt. + var build_result: Variant = runtime.submit_command(&"build_facility", {&"definition_id": &"reception", &"origin": Vector3i(-8, 0, 0)}) + _expect(build_result.is_success(), "real facility construction starts: " + str(build_result.to_dictionary())) + clock.advance_minutes(120) + 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 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 != &"departure_guest" and not guest.get("_ids").has(&"departure_guest"), "departed ID is absent while live roster fallback remains valid") + if not guest.selected_guest_id.is_empty(): + var live_report: Dictionary = runtime.guest_service.get_observation(guest.selected_guest_id) + _expect(not live_report.is_empty() and ("%dG" % int(live_report[&"wallet_gold"])) in guest.details.text, "fallback displays the live guest wallet") + # Create an empty-roster boundary through real departures, never array erasure. + for live: GuestRuntime in runtime.guest_service.get_guests(): + _expect(runtime.guest_service.begin_leaving(live.guest_id, clock.absolute_minute + 3, &"TEST_EXIT").get(&"ok", false), "empty-roster fixture begins departure") + _expect(runtime.guest_service.despawn_guest(live.guest_id, clock.absolute_minute + 4).get(&"ok", false), "empty-roster fixture completes departure") + guest.refresh() + _expect(guest.selected_guest_id.is_empty() and guest.guests.selected == -1 and guest.guests.disabled, "empty roster has no current selection or dropdown target") + _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)) + 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..c0efbdd --- /dev/null +++ b/tools/check_p24_selection_evidence.py @@ -0,0 +1,202 @@ +#!/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) + # Formal independent review follows machine validation in Runner. + # Keep its condition explicit without requiring a prior duplicate review. + if name == 'final_validation_review': + require(row.get('status') in ('pending_review', 'executed') and row.get('artifacts'), 'final regression evidence missing') + else: + 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', 'CUA-native-app'), 'OS collector/source mismatch') + cua = os_report.get('collector') == 'CUA-native-app' + if cua: + require(os_report.get('trace_provenance') in ('original_tool_receipts', 'faithful_transcription') and isinstance(os_report.get('limitations'), str) and os_report['limitations'].strip(), 'CUA trace provenance/limitations required') + 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') + require(any(e.get('action') == 'save' and e.get('pid') == observations[0]['pid'] for e in events) and any(e.get('action') == 'resume' and e.get('pid') == observations[1]['pid'] for e in events), 'OS save/resume must target producer/consumer respectively') + for event in events: + require(type(event.get('pid')) is int and event['pid'] in [r['pid'] for r in observations] and type(event.get('monotonic_ns')) is int and event['monotonic_ns'] > 0, 'OS target/time mismatch') + require(event.get('native_type') in ('mouseDown', 'keyDown'), 'engine function/emit is not OS input') + if cua: + require(event.get('delivery') == 'native_os' and event.get('injected') is True, 'CUA must disclose tool-generated native OS delivery') + step = read(artifact(event['tool_step'])) + require(step.get('pid') == event['pid'] and step.get('action') == event['action'] and step.get('monotonic_ns') == event['monotonic_ns'], 'CUA step target/action/time mismatch') + require(step.get('api') == 'native_app' and step.get('operation') in ('click', 'key') and isinstance(step.get('arguments'), dict) and step['arguments'], 'actual native app call arguments missing') + require(step.get('screenshot') == event['screenshot'] and step.get('source_map') == current, 'CUA returned screen/source mismatch') + else: + require(type(event.get('injected')) is bool and event.get('delivery') == 'native_os', 'OS delivery/origin disclosure required') + frame = artifact(event['screenshot']) + require({event['screenshot'], event['raw_trace']} <= set(matrix['normal_os_input']['artifacts']), 'OS condition lacks actual frame/trace artifacts') + if cua: + require(event['tool_step'] in matrix['normal_os_input']['artifacts'], 'OS condition lacks native app step artifact') + 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') + + require(set(matrix['final_validation_review']['artifacts']) >= {regression['input'], regression['ended'], regression['rows']}, 'final condition lacks full regression artifacts') + review_status = 'pending' + if matrix['final_validation_review']['status'] == 'executed': + review = read(artifact(index['independent_review'])) + require(index['independent_review'] in matrix['final_validation_review']['artifacts'], 'final condition lacks review artifact') + 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') + review_status = 'executed' + return {'machine_validation': True, 'formal_acceptance': False, 'conditions': list(CONDITIONS), 'independent_review': review_status} + + +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..dd284ae 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": "3137f36f547514f25ce6ac775fa7a5141d12f83ca4b95da0be45fecfafe9d9b4", + "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,