P24-03: 選択と保存の受入
This commit is contained in:
@@ -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> checks, <failures> 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> checks, <failures> failures",
|
||||
|
||||
+36
@@ -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
|
||||
}
|
||||
+16
@@ -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"
|
||||
}
|
||||
@@ -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 <observationをsort_keys=True,ensure_ascii=Falseのjson.dumpsで表現>` を収録する。作成者はこのログを生成していない。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結果から推測しない。
|
||||
+7
-2
@@ -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)):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c3p24departur
|
||||
@@ -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())
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user