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 3ae0fd5..2eecb80 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 @@ -3724,6 +3724,17 @@ "scope": "normal placement; native and OS pending" } } + ,{ + "test": "tests/ui/test_p23_life_observation.gd", + "template": "P23 life observation: checks, failures", + "required": true, + "fixed_check_count_used": false, + "source_sha256_at_schema_audit": "750254af7d4b8b00508e25d1ef744837d3a7d14ddebe12398e8014bf5e8c68e8", + "authority": { + "path": "docs/spec/acceptance/P23-03.json", + "scope": "component and read-only observation only; native unexecuted; whole unit blocked" + } + } ], "unknown_roster_suite_policy": "refuse, explicitly review its source/schema before accepting", "roster_count_is_not_hardcoded": true, diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/API_AND_STATE_MAP.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/API_AND_STATE_MAP.md new file mode 100644 index 0000000..3a09209 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/API_AND_STATE_MAP.md @@ -0,0 +1,41 @@ +# P23-03 Phase 0 — API and state map + +Base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. Static inspection only; no engine execution. + +## Exact source references + +Line numbers below refer to the base; SHA256 covers the complete file. + +| Source | Lines / API | SHA256 | +| --- | --- | --- | +| autoload/simulation_runtime.gd | 203 initialize(clock,state,seed); 225 seed assignment; 466 submit_command; 508 get_rng_stream; 511 create_snapshot; 1483 enable_production_operations; 1780 default demand configuration; 2831 arrival boundary; 2949 apply arrivals; 4137 queue_manual_checkpoint; 4141 save_to_path; 4169 load_from_path; 5066 seed streams; 6103 spawn_guest executor; 7629 build_facility executor; 10859 staff management report; 12464 supply management report | 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67 | +| autoload/game_state.gd | 20 reset | cf0e884910220e54c56b5875983e53de439af9d893be1e1b65c2121d2a30722f | +| scripts/core/simulation_kernel.gd | 91 reset; 104 advance_minutes; 122 advance_one_minute; 175 set_paused | 341c0c878c4894adbac628d0db6362ff69fc3347bdd9e61e50936e15dc2d1edb | +| scripts/facility/facility_service.gd | 109 get_instances; 656 create_snapshot | ceb4a3e9224587423f273cdb2b56e6843ab176ac1fc4fe79dd498b5ed5c33b77 | +| scripts/guest/guest_service.gd | 170 get_guests; 982 create_snapshot | 56880410e9a00816f9a95d30821982955532de8dd976980934e12016a9f2b21e | +| scripts/staff/staff_service.gd | 166 get_staff_members; 682 get_tasks; 1064 create_snapshot | 355ad5c3c9f85dc4d4ffc3bc4293e36f81078c403d1662512a735b2c8d815fb3 | +| scripts/transport/elevator_service.gd | 345 get_report; 362 create_snapshot | cea09dd9204551c26b31b30cd3aa847cc77e3b75fdac31662b9694a95fb3580c | +| scripts/world/path_service.gd | 545 get_cell_traffic | 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4 | +| scripts/supply/supply_logistics_bridge.gd | 122 get_report | b9e394af788595d2744b71f1907c8e7c3d762d38ff49992b1ee74e63b9639e80 | +| scripts/facility/facility_instance.gd | 8 OperationState; 10 State; 30 state and subsequent live fields | 0c6aa493f3299611e05dd26aa429dda8cf7b5c479e4d09632036fb862f79a324 | +| tests/transport/test_runtime_transport_comparison.gd | 91 existing fixed-demand setup; 109 real paid build command | ba577397fa8db5a6cf92cfd398dd7a5ece49e6b59856f14772b67b51ddc0bde9 | + +## Deterministic demand capability + +An existing deterministic input path is available without Runtime edits: fresh clock/state, `initialize(clock,state,seed)`, then the same scheduled `submit_command(&"spawn_guest", payload, ..., execute_at, idempotency_key)` sequence. The executor validates registered definitions and real walkable cells, creates wallets and GuestRuntime actors, and verifies Path actor projection and movement credit. Explicit IDs must avoid the reserved `guest-auto-` namespace. Inputs and processed results must both be recorded; a seed/hash alone does not prove identical arrivals. + +The existing transport comparison exercises public spawning, real paid construction and Runtime-owned movement. It is fixed-demand experimentation, not proof of natural autonomous demand or all six observation categories. Its numeric assertions are not adopted as new acceptance thresholds. Automatic production arrivals are a separate path (`enable_production_operations`): population, weather and terminal-state gates mean the same seed alone cannot guarantee identical accepted arrivals. Layouts must use identical production settings and compare actual arrival inputs/results. + +`build_facility` resolves the registered definition, runs `_build_facility_precheck`, then calls `construction_service.start_construction` with `definition.build_minutes`. Construction cost, time and capacities must come from definitions. B may add only the predeclared existing corridor/EV through this command. + +## Clock and observation + +`reset` defines initial time. Paused manual `advance_minutes` is explicitly supported; abort/pause boundaries must be checked. Commands run before minute increment, then time boundaries, mana, construction, logistics, facility completion, vitals, incidents, queues, AI and publication. + +Facility/guest/staff getters expose typed objects; presentation must copy values without mutating them. Elevator reports contain shafts/trips. Path traffic provides actual density/capacity/block fields. Logistics reports contain requests/deliveries, source/destination IDs, carrier, requirements, weight, state/reason and task ID. They do not contain warehouse stock: requested quantities must never be labelled stock. + +## Formal save/load and evidence + +Use `queue_manual_checkpoint` or `save_to_path`, then `load_from_path` under a distinct real PID. The load API pauses the clock and invokes SaveService package validation before restore. `restore_snapshot` alone is not formal resume. Preserve all package members, source/spec/engine/settings pins, whole typed sparse snapshots and actual CUA/process/cleanup evidence. + +Original references read: BASELINE.txt lines 10,13,16,25,37,43,69,83,106,216; IMPLEMENTATION_PLAN.md line 1800; immutable P23-03 spec; recorded direction 83e836e. All seven conditions remain mandatory, with no partial credit. Root native/OS/resume/full-regression evidence is missing. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_MIRROR_106_DIAGNOSTICS.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_MIRROR_106_DIAGNOSTICS.json new file mode 100644 index 0000000..8f9cc12 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_MIRROR_106_DIAGNOSTICS.json @@ -0,0 +1 @@ +{"task_id": "P23-03", "acceptance": "pending", "status": "blocked", "inventory_count": 106, "path_map": [{"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-profile.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-profile.json", "sha256": "f4c8e6290c983b6aa81a4352023adffb39bf70f53977a589eb5c48a212275792"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/ack.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/ack.json", "sha256": "6ba82c6625a1c93a0e1d77e9544fa711938d8b9b3792814ebb5e6aff41d563a3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-draft.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-draft.json", "sha256": "f970aa0b7f2ee3e1f818b7d349a9b7dd16d71904ac63d5451a3667095f43a10a"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-receipt.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-receipt.json", "sha256": "69df3dffe3056bad787bac1ff28a1cebdebcf634dcd7801fa9388a3148abecd3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/observed.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/observed.json", "sha256": "9ad55b933e90b4ebf5d375cf05e57fad4ea7f19ec0ec775ccc33f52d6e22ade4"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/request.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/request.json", "sha256": "3957f8618ab143d0c69e78686cb3126ae03ef50364990d5288a46f5bad22b143"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-after.png", "sha256": "3bf5460cbf5303d15bca7a553e3af9ff10c1dd33f36caca330b56c1aeda415b0"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-before.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-before.png", "sha256": "29eb60a401b63e3be6813a3c69341a9c53f77240a9e0503f6b5f2332f283640f"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/ack.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/ack.json", "sha256": "1a872d6cf63b70890c45180110b1332987c47f1ef289de6696ddc94b30445eb1"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-draft.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-draft.json", "sha256": "5e98491839e3fd9872bea2f2109076044790090a8d270653e50f4e76cb541bd6"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-receipt.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-receipt.json", "sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/observed.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/observed.json", "sha256": "6f6c980881ead5e6870edd2dd7900eae01358564b5c27c49a096471e5dc023eb"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/request.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/request.json", "sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", "sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stderr", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stderr", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stdout", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stdout", "sha256": "4c6cf2995559b9f9dfafbbe3bcea72c7c070316f2cceac3990b2431949b77eff"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", "sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-after.png", "sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-before.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-before.png", "sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/ack.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/ack.json", "sha256": "7bb69a521a6cf7b665ce135b004b935ddc6cd631c12d6e341a303c9e8b8b4638"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-draft.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-draft.json", "sha256": "0a414781f1a28f2d74d4ad141375e53bc991d0d08cca5149300893d52f72b6cf"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-receipt.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-receipt.json", "sha256": "bd7801304e9a79b403005ed19b269b342ce95773eaef6e26763644b690855d79"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/observed.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/observed.json", "sha256": "f1305e1d9986cf603d21e50525c26c79224c4e6596fc07bc86dc3c6db8cd0419"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/request.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/request.json", "sha256": "d331fd7e3419e6fc45da756c69298933975ae83e6af0b2dd6ccb908fa3c07695"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-after.png", "sha256": "0d288c1632689abae946fcc5aa2d3fa41ba42969c52ebc530f8cef1295ca1642"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-before.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-before.png", "sha256": "7531134f5251352f16a207de41ce15f400a8b8d5f673866d7f8ad708819a158e"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/ack.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/ack.json", "sha256": "80fd3d3d099871a9c6ba6a9b453159f682c54ee05f799cc56cc1ebc2c0a15806"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-draft.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-draft.json", "sha256": "50d9979223ef08632b11e61f340fa1fa7ff3d06aa74896b9128cc5c334379854"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-receipt.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-receipt.json", "sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/observed.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/observed.json", "sha256": "d6d33f52e224fccff81ed23ad62223c0a23e11171aef74583dfd0d8e9102a635"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/request.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/request.json", "sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-after.png", "sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-before.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-before.png", "sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/manifest.jsonl", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/manifest.jsonl", "sha256": "2a2e56d54c74caecc505215bc02a0415c4b226398559a2cdecb02d8a89224105"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7.json", "sha256": "006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627.json", "sha256": "07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2.json", "sha256": "081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14.json", "sha256": "0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c.json", "sha256": "0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925.json", "sha256": "209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1.json", "sha256": "277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55.json", "sha256": "3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec.json", "sha256": "36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6.json", "sha256": "3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1.json", "sha256": "3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1.json", "sha256": "42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5.json", "sha256": "48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378.json", "sha256": "4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d.json", "sha256": "533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e.json", "sha256": "59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94.json", "sha256": "5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a.json", "sha256": "5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237.json", "sha256": "6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3.json", "sha256": "60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610.json", "sha256": "6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041.json", "sha256": "6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", "sha256": "74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5.json", "sha256": "755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62.json", "sha256": "81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312.json", "sha256": "822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2.json", "sha256": "851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929.json", "sha256": "8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f.json", "sha256": "9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13.json", "sha256": "9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d.json", "sha256": "a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c.json", "sha256": "a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12.json", "sha256": "b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3.json", "sha256": "b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59.json", "sha256": "b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be.json", "sha256": "b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb.json", "sha256": "bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547.json", "sha256": "c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c.json", "sha256": "c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df.json", "sha256": "cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15.json", "sha256": "d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f.json", "sha256": "d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d.json", "sha256": "dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034.json", "sha256": "dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5.json", "sha256": "e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8.json", "sha256": "eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f.json", "sha256": "ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b.json", "sha256": "f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5.json", "sha256": "f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74.json", "sha256": "f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e.json", "sha256": "f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607.json", "sha256": "f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0.json", "sha256": "f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb.json", "sha256": "fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae.json", "sha256": "fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/ended.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/ended.json", "sha256": "9f1153d30fba11684fd287a5d8e60f5a38ff43bb39a3637009c7f1f71153e918"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/engine-native.log", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/engine-native.log", "sha256": "443b60a377ba87bde69d410fdfc4402a949a9d65aa053da69a5fd822703de098"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/passive_os.gd", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/passive_os.gd", "sha256": "b90f054faa4fd46e6a30ba12d2ce22cdcfb281351b209650c8a00a7713767e47"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/process-started.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/process-started.json", "sha256": "9ccb872bdb78e164fbf3a92d642df2c702148dc58ad62d163f48bde53de946a4"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/started.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/started.json", "sha256": "108f1968f89381feb50fb72ada1f6a8d019376c7c941c21f00ca186f21589169"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", "sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", "sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stdout", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stdout", "sha256": "b373c1f4a8afd7ed420b13ea1ac3221c6d8d34e7be1080fc392bf3ec78dfd367"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stderr", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stderr", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", "sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", "sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stdout", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stdout", "sha256": "eff26a97dc0e3e17cb5071009e539d3129c80af17752f6fa686c2caaf678e078"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stderr", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stderr", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", "sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", "sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stdout", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stdout", "sha256": "4e12bec18012e2c609e66c0250f0dd7c3201bbf8d32e7241507926f169a30c2d"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stderr", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stderr", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}], "inspected_eight": [{"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", "sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", "sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", "sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", "sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", "sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", "sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", "sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c"}, {"original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", "sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95"}], "frames": [{"request": "writer-request-0002", "layer": 0, "rows": [{"category": "room", "id": "facility_00000002", "eligibility": "visible", "local_rect": [131.0, 388.0, 256.0, 192.0], "screenshot_rect": [210.0, 485.0, 256.0, 192.0], "operation": "INACTIVE", "dimmed": true, "noise": {"samples": 49152, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 49152}}, {"category": "room", "id": "facility_00000003", "eligibility": "visible", "local_rect": [1027.0, 388.0, 256.0, 192.0], "screenshot_rect": [1106.0, 485.0, 256.0, 192.0], "operation": "INACTIVE", "dimmed": true, "noise": {"samples": 49152, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 49152}}, {"category": "room", "id": "facility_00000007", "eligibility": "visible", "local_rect": [131.0, 292.0, 128.0, 96.0], "screenshot_rect": [210.0, 389.0, 128.0, 96.0], "operation": "OPERATING", "dimmed": false, "noise": {"samples": 12288, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 12288}}, {"category": "queue", "id": "0", "eligibility": "visible", "local_rect": [259.0, 276.0, 42.0, 14.0], "screenshot_rect": [338.0, 373.0, 42.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 588, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 588}}, {"category": "guest", "id": "guest/guest-auto-00000019", "eligibility": "visible", "local_rect": [267.0, 325.0, 48.0, 54.0], "screenshot_rect": [346.0, 422.0, 48.0, 54.0], "operation": null, "dimmed": false, "noise": {"samples": 2592, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 2592}}, {"category": "guest", "id": "guest/guest-auto-00000020", "eligibility": "visible", "local_rect": [139.0, 301.0, 48.0, 54.0], "screenshot_rect": [218.0, 398.0, 48.0, 54.0], "operation": null, "dimmed": false, "noise": {"samples": 2592, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 2592}}, {"category": "business", "id": "0", "eligibility": "visible", "local_rect": [133.0, 390.0, 79.0, 14.0], "screenshot_rect": [212.0, 487.0, 79.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 1106, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 1106}}, {"category": "business", "id": "1", "eligibility": "visible", "local_rect": [1029.0, 390.0, 79.0, 14.0], "screenshot_rect": [1108.0, 487.0, 79.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 1106, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 1106}}, {"category": "business", "id": "2", "eligibility": "visible", "local_rect": [133.0, 294.0, 75.0, 14.0], "screenshot_rect": [212.0, 391.0, 75.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 1050, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 1050}}], "unaudited_traffic_overlays": [{"cell": [-6, 2, 0], "rect": [261.0, 294.0, 60.0, 92.0]}], "pending_categories": ["EV", "staff", "goods"], "pixel_class_state_proof": "pending; temporal noise is not class/state proof"}, {"request": "writer-request-0004", "layer": 1, "rows": [{"category": "room", "id": "facility_00000004", "eligibility": "visible", "local_rect": [131.0, 388.0, 256.0, 192.0], "screenshot_rect": [210.0, 485.0, 256.0, 192.0], "operation": "INACTIVE", "dimmed": true, "noise": {"samples": 49152, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 49152}}, {"category": "room", "id": "facility_00000005", "eligibility": "visible", "local_rect": [1027.0, 388.0, 256.0, 192.0], "screenshot_rect": [1106.0, 485.0, 256.0, 192.0], "operation": "INACTIVE", "dimmed": true, "noise": {"samples": 49152, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 49152}}, {"category": "business", "id": "0", "eligibility": "visible", "local_rect": [133.0, 390.0, 79.0, 14.0], "screenshot_rect": [212.0, 487.0, 79.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 1106, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 1106}}, {"category": "business", "id": "1", "eligibility": "visible", "local_rect": [1029.0, 390.0, 79.0, 14.0], "screenshot_rect": [1108.0, 487.0, 79.0, 14.0], "operation": null, "dimmed": false, "noise": {"samples": 1106, "max_channel_delta": 0, "mean_channel_delta": 0.0, "unchanged": 1106}}], "unaudited_traffic_overlays": [], "pending_categories": ["queue", "EV", "staff", "goods"], "pixel_class_state_proof": "pending; temporal noise is not class/state proof"}], "root_native_os_full_regression_independent_review": "pending"} diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_0b7727a_BLOCKED.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_0b7727a_BLOCKED.md new file mode 100644 index 0000000..e93f5cd --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_0b7727a_BLOCKED.md @@ -0,0 +1,253 @@ +# P23-03 offline repair — evidence gap + +Status: blocked, not static-complete. No acceptance credit. The failed 0b7727a +OS run remains failure history. No receipts, mirror files, product, values, +styles or test gates were changed. No git writes or engine/OS/collector/full +runs were performed. + +## Corrected direction + +The three previous pixel assumptions are superseded: + +1. `viewport_to_screen` maps to content coordinates, not screenshot pixels. + Actual request-0002 and request-0004 CUA receipt calibration records window + size 1440x810, screenshot 1440x842, titlebar 32, backing scale 1 and stretch 1. + The image lookup must apply the calibrated content origin and independently + check agreement with actual CUA click offsets; the number 32 must not become + a checker constant. Existing affine composition checks remain required. +2. Source RGB cannot be required unchanged after JPEG conversion, NO_STAFF + dimming, ghost outlines and antialiasing. Palette identity is source + provenance, not a screenshot color oracle. Pixel proof needs measured + within-image noise and independent expected-class/state/shift alternatives. +3. Every report row must be accounted for, but not every row is visible. Use + report/audit fields for visible, clipped/clamped, outside with reason, + ghost/inactive and facility-state dimming. Require eligible visible coverage + of all six categories on both FRONT and BACK. Never silently skip a row. + +The spec requires draw-consumed rows, pixel rectangles and provenance to +correspond; it does not require unchanged source RGB. The function docstring +now explicitly warns that the implementation still uses the superseded checks. + +## Verified material and blocking dependency + +Mirror: +`/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07` + +All 94 files named by MIRROR_INDEX.json were read and their SHA256 verified; +zero missing files or mismatches. The only indexed conversion record is +`os-pair/A/writer-request-0002/root-early-conversion.json`, with stdout/stderr. +The only decoded PNG is the corresponding `root-early-decoded.png`, SHA256 +`206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a`. + +The other three required stable screenshots are JPEG bytes despite .png names: + +- `os-pair/A/writer-request-0002/screen-after.png` +- `os-pair/A/writer-request-0004/screen-before.png` +- `os-pair/A/writer-request-0004/screen-after.png` + +Their magic starts `ffd8ffe000104a46`. No frozen decoded PNG/conversion records +for these files are in the mirror. Searching the original source root named +by MIRROR_INDEX for `*decoded*.png` and `*conversion*.json` also returned none. +The existing conversion verifier requires a separate PNG, original raw hash, +CUA call attribution and pinned `/usr/bin/sips` conversion record. Creating +replacement conversion assertions, rewriting receipt paths, or silently +changing that converter authority would not preserve this proof gate. + +Minimal next input: Root supplies separate decoded PNGs and genuine conversion +records/logs for those three existing raw images, adds their hashes to a new +mirror inventory while preserving the original inventory and receipts. This +does not require another OS capture. Alternatively the commander must explicitly +specify an offline Python JPEG decoder authority and its provenance contract. +Worker must not execute sips under the Python-only direction. + +Until then the requested full frozen before/after comparison, BACK cropped +pixel fixtures, calibrated noise and per-row pixel results cannot be submitted +as complete. Geometry/report preparation alone cannot meet all requested tests. +The behavioral repair and new fixtures/tests have not been implemented. + +## Local commands and outputs + +These read-only Python checks used the mirror path above as `p`: + +```python +import json, pathlib, hashlib +p = pathlib.Path('/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07') +j = json.loads((p / 'MIRROR_INDEX.json').read_text()) +bad = [r['path'] for r in j['files'] if not (p/r['path']).is_file() + or hashlib.sha256((p/r['path']).read_bytes()).hexdigest() != r['sha256']] +print('indexed', len(j['files']), 'mismatches', bad) +print('conversion records', [r['path'] for r in j['files'] if 'conversion' in r['path']]) +``` + +Output: +``` +indexed 94 mismatches [] +conversion records ['os-pair/A/writer-request-0002/root-early-conversion.json', 'os-pair/A/writer-request-0002/root-early-conversion.json.stderr', 'os-pair/A/writer-request-0002/root-early-conversion.json.stdout'] +``` + +Calling the existing `screenshot_pixels(p/name)` for each of the three paths +above (without manufacturing a conversion argument) produced: +``` +writer-request-0002/screen-after.png: JPEG screenshot requires conversion record +writer-request-0004/screen-before.png: JPEG screenshot requires conversion record +writer-request-0004/screen-after.png: JPEG screenshot requires conversion record +``` + +Command: `python3 tools/test_p23_life_observation_evidence.py` +``` +................................ +---------------------------------------------------------------------- +Ran 32 tests in 0.058s + +OK +``` + +These are existing unit tests, not the requested new repair tests, and do not +prove the repair or acceptance. Root native/OS/full review remains pending. +Seven conditions, six categories, 30/88, no partial credit, timeout 900 and +cap null remain unchanged. + +## Follow-on: 106-entry mirror (2026-10-15) + +The missing-conversion dependency above is **resolved**, not a current blocker. +The prior text is retained as failure history. The worker read all 106 indexed +files and verified every byte count and SHA256, plus the preserved initial94 +inventory SHA and unchanged entries. No mirror or original receipt was edited. + +`OFFLINE_MIRROR_106_DIAGNOSTICS.json` contains the explicit original absolute +path -> mirrored absolute path -> equal indexed SHA256 mapping for all106 files, +including the eight named PNG/conversion files. Each conversion was checked +against its corresponding actual CUA screenshot call ID/raw hash, pinned sips +binary SHA, original argv, output PNG magic/dimensions/hash and stdout/stderr +hashes. Original raw/png path strings and argv remain unchanged. A read-only +resolver locates indexed mirrored bytes; it does not rewrite conversion records. +Direct reads of the source artifact directory were denied by the host +(`PermissionError: Operation not permitted`); equality to the frozen inventory +and conversion-record hashes was verified, not a fresh direct source-directory +comparison. This limitation is not represented as a missing PNG conversion. + +New `--offline-mirror` preparation diagnostics check content/image dimensions, +backing scale, content origin derived from recorded titlebar, affine composition, +and agreement with the separately recorded click events. They independently +derive existing audit row identities/local rectangles from report positions and +source palette provenance. Every existing audit row has visible/clipped/outside +accounting and a calibrated screenshot rectangle. Room enum/operation names are +read from the actual FacilityInstance source. Dimming is derived from operation, +not merely the NO_STAFF state name. These are preparation diagnostics, not a +replacement acceptance pixel gate. In particular, audit font_width remains an +engine assertion, and receipt click events have not been verified against the +full helper-definition/tool trace. No source RGB tolerance was invented. + +Actual frozen-frame results: + +- FRONT request0002: 9 audit rows (3 room,1 queue,2 guest,3 business), all visible. + EV/staff/goods coverage pending. Two rooms are INACTIVE and one OPERATING. + One actual draw-consumed traffic overlay at cell[-6,2,0], local rect + [261,294,60,92], has no corresponding rectangle-audit row. +- BACK request0004: 4 audit rows (2 room,2 business), all visible and both rooms + INACTIVE. Queue/EV/staff/goods coverage pending; no eligible traffic overlay. +- Before/after temporal differences within all13 calibrated audit rectangles + are zero. This establishes stability only, not spatial JPEG error or correct + drawing class/state. It supplies no pixel acceptance credit. +- The existing independent typed-stream checker recomputed all14 frozen stream + records successfully; the CLI still returned blocked/pending(exit2), as required. + +Current repair status: **incomplete / blocked**, not genuinely submitted. +The legacy `compare_rectangles` and its normal-OS call site have deliberately +not been relabelled as repaired. Remaining worker work includes an independent +expected-class/state/shift pixel discriminator with measured within-image noise, +state negatives, full ghost/inactive/traffic draw accounting, independent badge +width provenance and integration into the strict OS checker. The added +preparation path does not solve these items and must not be used for acceptance. +Missing frozen provenance includes the FRONT traffic-overlay rectangle audit, +opposite-layer ghost draw audit, and independent font measurements for badge +widths. Existing reports/source can support further preparation; these missing +records do not establish that a fresh OS capture is already necessary. + +Local validation: Python AST syntax checks; 42 checker unit tests (existing32 +plus10 preparation tests); actual mirrored checker commands above. Tests cover +wrong titlebar/click shifts, PID, nonfinite scales, clipped/outside rectangles, +measured temporal noise, report rectangle/identity/coverage/palette tampering. +They do not include the still-required pixel-state discriminator negatives. +No Godot, native, sips execution, collector, OS run, full regression, git write, +branch/integration, Runner action or agent delegation occurred. Root native, +complete A/B FRONT/BACK pan/zoom/distinct-PID OS matrix, mandatory full regression +and independent Opus review remain pending. Historical baselines remain32 tests, +17+17 semantic,319 native,7153/2585 and7/6/30/88; the new local42 count is not a +replacement acceptance baseline. Timeout900 and cap null are unchanged. + +## Owner proof-method repair (2026-10-15) + +The owner's recorded decision supersedes the proposed automatic class/state/ +style discriminator, shift/swap alternative scoring, measured-noise gate and +independent badge-width measurement gate. They are no longer required work. +Palette RGB is provenance only. Font width remains the recorded engine value; +no independent font measurement or numeric image tolerance was introduced. +All earlier entries above are retained as history, not current requirements. + +Implemented within the narrowed Python edit scope: + +- Shared `frame_rows` now handles both offline and normal OS screenshot frames, + uses receipt calibration checked against actual click events, checks affine + composition, independently derives row identity/local geometry/text/palette, + validates room state/operation and actor enum names, and retains every audit + row with visibility, clipping reason, calibrated image rectangle and failures. +- The legacy `compare_rectangles` and its tests are removed and replaced by + grounded negatives. Fully outside eligible image crops and exactly uniform + crops fail; source RGB is never compared to screenshot RGB. +- `drawing_rectangle_rows` now expects every draw-consumed traffic overlay. + Missing/extra rows fail the mechanical frame, without synthesizing audit rows. +- Pure-Python PNG crops and review manifests are written for each frozen frame. + Manifests bind decoded whole-image/raw/conversion/request/receipt hashes, + per-row source hash, recorded font width, state, operation, layer and geometry. + A separate SHA file hashes the exact manifest bytes. Normal OS frames use the + same path and emit per-image review materials. Pixel categories receive no + credit without an independent direct-image review verdict bound to that SHA, + covering every eligible row with category/state/position verdicts. The worker + created no actual review verdict; synthetic verdict files exist only inside + temporary unit-test fixtures. + +Current scope blocker, found by reading the actual dependencies: +`tests/fixtures/p23-03/passive_observation.gd:150` owns `draw_rectangles`, not +`world_preview.gd`. Its explicit room/queue/cabin/actor/badge loops at lines +152-171 do not consume traffic audit rows from the view or report. The transform +and palette audit path at lines172-182 only processes that locally built array. +`world_preview.gd:762` draws the traffic overlay, but has no existing rectangle +audit emitter or callback. Changing only world_preview cannot connect traffic +rows to the actual request's `rectangles` field without altering the fixture. +No inert emitter, mislabelled badge, or checker-generated observed audit was +added to pretend this connection exists. World source remains unchanged. + +Minimal separate scope proposal: permit one read-only traffic-row loop in +`passive_observation.gd::draw_rectangles`, before its existing transform/palette +loop, consuming report traffic rows with density>0 or temporary_block. Use +category `traffic`, comma-separated cell coordinates as id, the report rectangle +and `scenes/main/world_preview.gd` as style source. Alternatively explicitly +authorize wiring a view-owned traffic audit emitter into that same fixture. +The broad contract lists this fixture, but the latest finish recipe explicitly +narrows edits to checker/tests and world only; the worker did not override it. +Step4 is therefore incomplete and this repair is **blocked**, not submitted. + +Local validation: both Python files parse with `ast.parse`; 47 local checker +tests pass. New tests exercise blank crops, missing/extra/traffic rows, enum/name +errors, affine/click calibration shifts, image-outside crops, exact crop hashes, +wrong-manifest/missing/extra/rejected review rows and no credit without review. +The r07 offline CLI verified all106 indexed entries and exited2 as blocked: + +- FRONT request0002: expected10 rows, observed9; mechanical failure + `offline rectangle coverage`. The traffic overlay at [-6,2,0] remains a genuine + unaudited overlay. EV/staff/goods coverage pending; zero pixel credit. +- BACK request0004: expected4/observed4; mechanical checks pass. + Queue/EV/staff/goods coverage pending; zero pixel credit. + +Current review artifacts are in `offline-r07-review/`; complete diagnostics are +in `OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json`. The prior +`OFFLINE_MIRROR_106_DIAGNOSTICS.json` is preserved unchanged. + +Still pending: Root frozen native checks, fresh A/B FRONT/BACK ordinary pan/zoom +OS observation and formal distinct-PID resume, the exact full177 regression, +and independent Opus direct-image/all-condition review. No Godot, Runner, +native/OS, full regression, git write, integration or agent execution occurred. +All7 conditions, all6 categories on both layers/layouts, source recomputation, +raw/request/CUA/conversion provenance, calibrated geometry, 30/88 without partial +credit, timeout900 and cap null remain unchanged. No acceptance PASS is claimed. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_55c29f1.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_55c29f1.md new file mode 100644 index 0000000..dd8a45b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_55c29f1.md @@ -0,0 +1,72 @@ +# Offline checker repair on 55c29f1 + +Source HEAD: 55c29f119234dd6f993c604c4edc6f377fe3951c. Registration base remains +860136f7099f5e3aee99da5798a627a77f2d8eea. No git writes or engine execution. + +Read access: MIRROR_INDEX.json and all 2,622 named files were successfully read +under `/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-evidence-55c29f1-r06`. +No mirror bytes were changed. The initial shell heredoc could not create its +temporary file; the read check was executed using Python `-c` instead. + +## First defects and production cause + +Both layouts first failed at frame-030, checker line 328, with KeyError: +`shaft_id`. State objects: + +- A: `layout-A/objects/ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3.json` +- B: `layout-B/objects/97ea43afe3d1b21602ebe2a602e7b7d796ff05f03983cd4d1b3edc9157e39a08.json` + +The exception came from `shaft['shaft_id']`, not `t['shaft_id']`. Every trip in +these failing frames has shaft_id. Saved shaft rows have it only in config. +There is no evidence of shaft-less QUEUED trips to support accepting that shape. + +`autoload/simulation_runtime.gd:541` writes elevators.create_snapshot(). +`scripts/transport/elevator_service.gd:362-367` copies the saved cars without +flattening config; get_report at 345-359 merges config into each report row before +counting queued trips. request at 169-171 assigns shaft_id on creation, and the +restore validator at 412-454 requires the exact trip keys and shaft/connector +agreement. The connector spelling is defined at 617-618; trips are ordered at +707-710. `scenes/main/world_preview.gd:973-975` consumes get_report and sorts +trips by enqueue time/actor. The checker now counts using the merged row identity. + +## Reviewable changes + +- `tools/check_p23_life_observation_evidence.py`: replace + `t['shaft_id'] == shaft['shaft_id']` with `t['shaft_id'] == row['shaft_id']`; + validate exact trip keys, companion identity, shaft/stops/connector attribution + and QUEUED/BOARDED state before recomputing. Whole report equality remains strict. +- `tests/fixtures/p23-03/queued_elevator.json`: retain one actual frame-030 + queued trip and its shaft, without a cabin seat. Distilled unit data only. +- `tools/test_p23_life_observation_evidence.py`: queued shape positive and + shaft attribution, waiting_count, missing/extra key negatives; update two + pre-existing abbreviated synthetic fixtures to the actual shaft/trip shape. +- This repair report. The complete implementation diff is available through + `git diff -- tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`; + the new fixture and this file are untracked submission files. + +## Local results + +`inspect_stream` on each exact absolute mirror directory: + +``` +layout-A inspect_stream PASS 17 frames +layout-B inspect_stream PASS 17 frames +``` + +For both, labels are frame-000, frame-030, frame-060, frame-090, frame-120, +frame-150, frame-180, frame-210, frame-240, frame-270, frame-300, frame-330, +frame-360, frame-390, frame-420, frame-450, frame-480. Each returns fields: +guests, staff, tasks, elevators, room_service_fields, queues, traffic, goods, +cabin_geometry. Every frame also passes the existing two-motion-history-frame +check, unchanged. No later schema/semantic mismatch was observed. + +Python syntax AST checks pass. Local checker tests: 32 tests, OK. +The initial test run exposed two abbreviated synthetic fixtures; subsequent +queue expectations were corrected for actual lift landing exclusion. Tests +were preserved. git diff --check passes; changed files are within allowed_paths. +completion_marker_contracts retains its one required P23 strict template entry; +no marker changes. Seven conditions, six categories and 30/88 remain unchanged. + +Root native results (319 tests, A/B PASS), matched metric values (48 metrics, +7153/2585), production data and services were not modified. Root OS evidence +remains pending as expected at worker submission; this repair grants no acceptance. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json new file mode 100644 index 0000000..6356b8c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json @@ -0,0 +1,1953 @@ +{ + "task_id": "P23-03", + "acceptance": "pending", + "status": "blocked", + "inventory_count": 106, + "path_map": [ + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-profile.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-profile.json", + "sha256": "f4c8e6290c983b6aa81a4352023adffb39bf70f53977a589eb5c48a212275792" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/ack.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/ack.json", + "sha256": "6ba82c6625a1c93a0e1d77e9544fa711938d8b9b3792814ebb5e6aff41d563a3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-draft.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-draft.json", + "sha256": "f970aa0b7f2ee3e1f818b7d349a9b7dd16d71904ac63d5451a3667095f43a10a" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-receipt.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/cua-receipt.json", + "sha256": "69df3dffe3056bad787bac1ff28a1cebdebcf634dcd7801fa9388a3148abecd3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/observed.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/observed.json", + "sha256": "9ad55b933e90b4ebf5d375cf05e57fad4ea7f19ec0ec775ccc33f52d6e22ade4" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/request.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/request.json", + "sha256": "3957f8618ab143d0c69e78686cb3126ae03ef50364990d5288a46f5bad22b143" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-after.png", + "sha256": "3bf5460cbf5303d15bca7a553e3af9ff10c1dd33f36caca330b56c1aeda415b0" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-before.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0001/screen-before.png", + "sha256": "29eb60a401b63e3be6813a3c69341a9c53f77240a9e0503f6b5f2332f283640f" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/ack.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/ack.json", + "sha256": "1a872d6cf63b70890c45180110b1332987c47f1ef289de6696ddc94b30445eb1" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-draft.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-draft.json", + "sha256": "5e98491839e3fd9872bea2f2109076044790090a8d270653e50f4e76cb541bd6" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-receipt.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/cua-receipt.json", + "sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/observed.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/observed.json", + "sha256": "6f6c980881ead5e6870edd2dd7900eae01358564b5c27c49a096471e5dc023eb" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/request.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/request.json", + "sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", + "sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stderr", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stderr", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stdout", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json.stdout", + "sha256": "4c6cf2995559b9f9dfafbbe3bcea72c7c070316f2cceac3990b2431949b77eff" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", + "sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-after.png", + "sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-before.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/screen-before.png", + "sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/ack.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/ack.json", + "sha256": "7bb69a521a6cf7b665ce135b004b935ddc6cd631c12d6e341a303c9e8b8b4638" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-draft.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-draft.json", + "sha256": "0a414781f1a28f2d74d4ad141375e53bc991d0d08cca5149300893d52f72b6cf" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-receipt.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/cua-receipt.json", + "sha256": "bd7801304e9a79b403005ed19b269b342ce95773eaef6e26763644b690855d79" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/observed.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/observed.json", + "sha256": "f1305e1d9986cf603d21e50525c26c79224c4e6596fc07bc86dc3c6db8cd0419" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/request.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/request.json", + "sha256": "d331fd7e3419e6fc45da756c69298933975ae83e6af0b2dd6ccb908fa3c07695" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-after.png", + "sha256": "0d288c1632689abae946fcc5aa2d3fa41ba42969c52ebc530f8cef1295ca1642" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-before.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0003/screen-before.png", + "sha256": "7531134f5251352f16a207de41ce15f400a8b8d5f673866d7f8ad708819a158e" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/ack.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/ack.json", + "sha256": "80fd3d3d099871a9c6ba6a9b453159f682c54ee05f799cc56cc1ebc2c0a15806" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-draft.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-draft.json", + "sha256": "50d9979223ef08632b11e61f340fa1fa7ff3d06aa74896b9128cc5c334379854" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-receipt.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/cua-receipt.json", + "sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/observed.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/observed.json", + "sha256": "d6d33f52e224fccff81ed23ad62223c0a23e11171aef74583dfd0d8e9102a635" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/request.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/request.json", + "sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-after.png", + "sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-before.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/screen-before.png", + "sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/manifest.jsonl", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/manifest.jsonl", + "sha256": "2a2e56d54c74caecc505215bc02a0415c4b226398559a2cdecb02d8a89224105" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7.json", + "sha256": "006e6935714b7de8da92695719f9446219a2ae851d83f1e93496cbd00c937bc7" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627.json", + "sha256": "07ec7d9753eb4173bc87f4138022ea05786ba3535d7a87f94c04b15d91504627" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2.json", + "sha256": "081adfbec19ca2d35db248e11dc7eec4c1ca73ef1183a02982a2ab64ee79c9d2" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14.json", + "sha256": "0a87879a4f0fd10e123f81c28fb1040429c2ec012a56aa008a5489b3d785fb14" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c.json", + "sha256": "0f0486675c258f53110faf64fe7e755d44604f009f8e2c6c2a3d69b399f0a40c" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925.json", + "sha256": "209555b7428701d2bcfabb0cd704eed7afe0ceaf3b8c6b8b646afcdb949f3925" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1.json", + "sha256": "277769564469430077eb639f3e530350d7ba971153fb5ebc76c8733b0fd6dfc1" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55.json", + "sha256": "3498cdb05642a0ae559a63bf42b50c13e008f0c9a24d6a97d56a8b1076d36a55" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec.json", + "sha256": "36e97c42c3c70c217bffb14431446d6da54d5623ce7a8df887bb4fc2ee91e1ec" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6.json", + "sha256": "3b76cdbb5d4a16559bf81bef3c28940aad0c29d3378e26de3daa911e3e900da6" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1.json", + "sha256": "3e1d048bf2c92d3c0b8826a6d29b67157e6ece4716a106b37c8abce54f7c07f1" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1.json", + "sha256": "42dc46c981c1a8abfa9108c6cbf148036debd92f43a1d0b202ff76b15b01d6e1" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5.json", + "sha256": "48313efc10bef15c3d8cf384de9ea591da1d9c3ff8a5539f12c6b3d1cf0c63e5" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378.json", + "sha256": "4f3f1601593b6ab0b25bfa9968ea2515edf6cf496080165b40f02b51ddd3b378" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d.json", + "sha256": "533f62b8ce93979e7823662864763d4d0a1f3354ea544c2784e185a679e1cf5d" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e.json", + "sha256": "59035f4021793807f5eb35f322dc1c8ae642bccf499cc2ab69dadbcbff16117e" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94.json", + "sha256": "5ef44a9c9c2e95a417c452603c875aac3de7818b8a36bc0dab921afb992d2f94" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a.json", + "sha256": "5f94d9b3217fb786d835eae33c05ced2b59eb09ef0486534de5f9616e8dda84a" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237.json", + "sha256": "6058bb5d828172e29449e2d650919c3e4043713902b5792becc02c8ceb0bf237" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3.json", + "sha256": "60beb433cbe02bb31dae1a37abc0089e33314c852474bcf35ce5a6c031ac3af3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610.json", + "sha256": "6d28e9048b255064f2cba6f75423f9a8a80164972abcbd8de27fbbfc93577610" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041.json", + "sha256": "6e3378d6aec1834b37003da05775bd59049cb4a8701360e3225a8731be87d041" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", + "sha256": "74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5.json", + "sha256": "755326d7c8f5d7a36bf743d3c67f24da8483f35f1235cefaa90c0db3babaf4d5" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62.json", + "sha256": "81ad6f4cf61b66632c8b42226830cb0e352d4b0a74a0de5af6f46796cbb20d62" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312.json", + "sha256": "822666890d5313a9a995d9f49b15a0d8074d991ea0bb7035dfd7af2a70bc3312" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2.json", + "sha256": "851014d0a62736af16750b0e321d43eeaee22a5132a338c1928e0c90333d1bd2" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929.json", + "sha256": "8e8ca9a9f1887d40aaf58f42d8ef94088b678a5a8ebbdff7357a9fa12df6f929" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f.json", + "sha256": "9256d4122881a7a8ecf0cde7aa17bf5021a67399d79cf664a3c118886ca08b7f" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13.json", + "sha256": "9d2a5477aad0c6d8090c5990c357e44640318dedae558021730f6c7c7fdeef13" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d.json", + "sha256": "a19556a94b9990baca58bd52672c85bec9dc5cdda98c5dee251648835a594b1d" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c.json", + "sha256": "a9bb07bf7b9c182da806c290b524f6dd0c339b9ce6b6360aab2a2e7fe352958c" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12.json", + "sha256": "b29ddb1f71715e4d78ce0f84e196d93db425680e7775eeae792fad2535618e12" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3.json", + "sha256": "b2bd7ca682726d7b8d4bb7d89bab95a5daf68b5894c70a6c2cee44bcfeabb7b3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59.json", + "sha256": "b3fc82ce2b7ef87eebfc6d6219f55537995c8aa6e8ebf720a42f2fde8c515f59" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be.json", + "sha256": "b581a8dce62302bb8054d386ae0e0384c8412a60daf92d72c5ac9ddbfa2631be" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb.json", + "sha256": "bf86175f744e64db3fd3a8d893faebe7eb77dba4c5afb3807a8bae0b298684eb" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547.json", + "sha256": "c56a3daafa29d180ae351a8184120d28a7be3c17493a3e791fdb0b2cdc986547" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c.json", + "sha256": "c5e44308042811dc780d3359d92b462e86d8d7682369a68dc419ee4de1479e9c" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df.json", + "sha256": "cf6cc0471a2eb41de41d3bd997676dc4d78b50fe304b8164530ead8fef5eb4df" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15.json", + "sha256": "d36802f06bab7f5dee837e6c2061320a1098b7ad659bb4b26b4d9d201f03fe15" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f.json", + "sha256": "d622acf31417035c3378339e89360e68e49d37653e511c9aaac0970122e8388f" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d.json", + "sha256": "dada5203240e91ae4cb2c4534e976013cacbcd8794bcf17d16e6971dfd6c4b1d" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034.json", + "sha256": "dff36d6a29cdc6f25c4979218cd3f4fe9aa6a3a8122f0e383ab46d23941ec034" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5.json", + "sha256": "e3ca732a987307c6d9cd59389dc480c11d3efcbcc65608ee3f4fc12efa2ce9d5" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8.json", + "sha256": "eb076505b0f853f33d000fa2332365e74cfd488cefeb5ab1f6c12e7cdb52f4d8" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f.json", + "sha256": "ee0fae657b55ee7728b35d11a9275c1c9f2027958fd6ec772c29b78fa7f1851f" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b.json", + "sha256": "f08aa2e3a04c005aac2f83c018dde863a762583d6b170970507be070db7c1c1b" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5.json", + "sha256": "f3accdc0150419eb72b3ac9054a55bd21aecd35c700b2d86074ae52c94b11ab5" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74.json", + "sha256": "f620959221ba482cd916bb01f53ef49eda7dc10a57b81f68922ad28cd18faa74" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e.json", + "sha256": "f74bd2860868160233adb8a1e783b739472550121e9ac769dc89f7c80ef6436e" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607.json", + "sha256": "f825a3aebd0f312b3d9108839e25329edc127b29b84716ccbaf400386f3af607" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0.json", + "sha256": "f8c3b035747623a97488f4aaed64e9daef4176f41c8c1239edd29061f987d6a0" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb.json", + "sha256": "fa523a9e4158e7d74c080b3cef7f9a1667cf8c21799c57c8fbde4fc7022c67cb" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-stream/objects/fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-stream/objects/fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae.json", + "sha256": "fb7ab245f13a8d72469bf2f4a03843799a7b0d406ea4eefcc2e2556be1df86ae" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/ended.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/ended.json", + "sha256": "9f1153d30fba11684fd287a5d8e60f5a38ff43bb39a3637009c7f1f71153e918" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/engine-native.log", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/engine-native.log", + "sha256": "443b60a377ba87bde69d410fdfc4402a949a9d65aa053da69a5fd822703de098" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/passive_os.gd", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/passive_os.gd", + "sha256": "b90f054faa4fd46e6a30ba12d2ce22cdcfb281351b209650c8a00a7713767e47" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/process-started.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/process-started.json", + "sha256": "9ccb872bdb78e164fbf3a92d642df2c702148dc58ad62d163f48bde53de946a4" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-runs/A-writer/started.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-runs/A-writer/started.json", + "sha256": "108f1968f89381feb50fb72ada1f6a8d019376c7c941c21f00ca186f21589169" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", + "sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", + "sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stdout", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stdout", + "sha256": "b373c1f4a8afd7ed420b13ea1ac3221c6d8d34e7be1080fc392bf3ec78dfd367" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stderr", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json.stderr", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", + "sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", + "sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stdout", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stdout", + "sha256": "eff26a97dc0e3e17cb5071009e539d3129c80af17752f6fa686c2caaf678e078" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stderr", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json.stderr", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", + "sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", + "sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stdout", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stdout", + "sha256": "4e12bec18012e2c609e66c0250f0dd7c3201bbf8d32e7241507926f169a30c2d" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stderr", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json.stderr", + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + } + ], + "inspected_eight": [ + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion.json", + "sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded.png", + "sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-conversion-after.json", + "sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0002/root-early-decoded-after.png", + "sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion.json", + "sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded.png", + "sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-conversion-after.json", + "sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c" + }, + { + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", + "mirror": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/offline-os-0b7727a-r07/os-pair/A/writer-request-0004/root-early-decoded-after.png", + "sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95" + } + ], + "frames": [ + { + "request": "writer-request-0002", + "layer": 0, + "manifest_sha256": "646334f4ef3c78a8cfe5d68d3738c4d772cdd71cbf80b2dba7c153693ead4419", + "manifest_path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/P23-03-implement/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.json", + "rows": [ + { + "row_index": 0, + "category": "room", + "id": "facility_00000002", + "layer": 0, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": true, + "text": null, + "local_rect": [ + 131.0, + 388.0, + 256.0, + 192.0 + ], + "screenshot_rect": [ + 210.0, + 485.0, + 256.0, + 192.0 + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286", + "path": "row-0000-image-0.png" + }, + { + "image_index": 1, + "sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286", + "path": "row-0000-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 1, + "category": "room", + "id": "facility_00000003", + "layer": 0, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": true, + "text": null, + "local_rect": [ + 1027.0, + 388.0, + 256.0, + 192.0 + ], + "screenshot_rect": [ + 1106.0, + 485.0, + 256.0, + 192.0 + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69", + "path": "row-0001-image-0.png" + }, + { + "image_index": 1, + "sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69", + "path": "row-0001-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 2, + "category": "room", + "id": "facility_00000007", + "layer": 0, + "state": "OPEN", + "state_name": "OPEN", + "operation": "OPERATING", + "dimmed": false, + "text": null, + "local_rect": [ + 131.0, + 292.0, + 128.0, + 96.0 + ], + "screenshot_rect": [ + 210.0, + 389.0, + 128.0, + 96.0 + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b", + "path": "row-0002-image-0.png" + }, + { + "image_index": 1, + "sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b", + "path": "row-0002-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 3, + "category": "queue", + "id": "0", + "layer": 0, + "state": { + "members": [ + "guest/guest-auto-00000019" + ], + "visible_count": 1, + "overflow": 0 + }, + "state_name": null, + "operation": null, + "dimmed": false, + "text": "待ち1人", + "local_rect": [ + 259.0, + 276.0, + 42.0, + 14.0 + ], + "screenshot_rect": [ + 338.0, + 373.0, + 42.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 36.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac", + "path": "row-0003-image-0.png" + }, + { + "image_index": 1, + "sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac", + "path": "row-0003-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 4, + "category": "guest", + "id": "guest/guest-auto-00000019", + "layer": 0, + "state": "QUEUING", + "state_name": "QUEUING", + "operation": null, + "dimmed": false, + "text": null, + "local_rect": [ + 267.0, + 325.0, + 48.0, + 54.0 + ], + "screenshot_rect": [ + 346.0, + 422.0, + 48.0, + 54.0 + ], + "style_source": "scripts/ui/pixel_people.gd", + "style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c", + "style_colors": [ + "688da0", + "b87659", + "a69a63", + "89789e", + "638d72", + "edc49d", + "c99371", + "a56f50", + "303c40", + "719d94", + "eee5c9", + "608d84", + "eee6c7", + "e3dfc2", + "956940", + "d3ad6b", + "b48850", + "af9059", + "d8bc78", + "f3ebcf", + "efe7ce", + "805e44" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34", + "path": "row-0004-image-0.png" + }, + { + "image_index": 1, + "sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34", + "path": "row-0004-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 5, + "category": "guest", + "id": "guest/guest-auto-00000020", + "layer": 0, + "state": "RECEIVING_SERVICE", + "state_name": "RECEIVING_SERVICE", + "operation": null, + "dimmed": false, + "text": null, + "local_rect": [ + 139.0, + 301.0, + 48.0, + 54.0 + ], + "screenshot_rect": [ + 218.0, + 398.0, + 48.0, + 54.0 + ], + "style_source": "scripts/ui/pixel_people.gd", + "style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c", + "style_colors": [ + "688da0", + "b87659", + "a69a63", + "89789e", + "638d72", + "edc49d", + "c99371", + "a56f50", + "303c40", + "719d94", + "eee5c9", + "608d84", + "eee6c7", + "e3dfc2", + "956940", + "d3ad6b", + "b48850", + "af9059", + "d8bc78", + "f3ebcf", + "efe7ce", + "805e44" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522", + "path": "row-0005-image-0.png" + }, + { + "image_index": 1, + "sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522", + "path": "row-0005-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 6, + "category": "business", + "id": "0", + "layer": 0, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": false, + "text": "職員不足 未稼働", + "local_rect": [ + 133.0, + 390.0, + 79.0, + 14.0 + ], + "screenshot_rect": [ + 212.0, + 487.0, + 79.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 73.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a", + "path": "row-0006-image-0.png" + }, + { + "image_index": 1, + "sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a", + "path": "row-0006-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 7, + "category": "business", + "id": "1", + "layer": 0, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": false, + "text": "職員不足 未稼働", + "local_rect": [ + 1029.0, + 390.0, + 79.0, + 14.0 + ], + "screenshot_rect": [ + 1108.0, + 487.0, + 79.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 73.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d", + "path": "row-0007-image-0.png" + }, + { + "image_index": 1, + "sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d", + "path": "row-0007-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + }, + { + "row_index": 8, + "category": "business", + "id": "2", + "layer": 0, + "state": "OPEN", + "state_name": "OPEN", + "operation": "OPERATING", + "dimmed": false, + "text": "営業中 利用1人", + "local_rect": [ + 133.0, + 294.0, + 75.0, + 14.0 + ], + "screenshot_rect": [ + 212.0, + 391.0, + 75.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 69.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5", + "path": "row-0008-image-0.png" + }, + { + "image_index": 1, + "sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5", + "path": "row-0008-image-1.png" + } + ], + "mechanical_pass": false, + "failures": [ + "offline rectangle coverage" + ] + } + ], + "mechanical_pass": false, + "errors": [ + "offline rectangle coverage" + ], + "unaudited_traffic_overlays": [ + { + "cell": [ + -6, + 2, + 0 + ], + "rect": [ + 261.0, + 294.0, + 60.0, + 92.0 + ] + } + ], + "pending_categories": [ + "EV", + "staff", + "goods" + ], + "pixel_categories": [], + "independent_review": "pending" + }, + { + "request": "writer-request-0004", + "layer": 1, + "manifest_sha256": "f013e0d1d6231833b656859cc73ad0b58b389047e421ad2c43cc598820e21067", + "manifest_path": "/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/P23-03-implement/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.json", + "rows": [ + { + "row_index": 0, + "category": "room", + "id": "facility_00000004", + "layer": 1, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": true, + "text": null, + "local_rect": [ + 131.0, + 388.0, + 256.0, + 192.0 + ], + "screenshot_rect": [ + 210.0, + 485.0, + 256.0, + 192.0 + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43", + "path": "row-0000-image-0.png" + }, + { + "image_index": 1, + "sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43", + "path": "row-0000-image-1.png" + } + ], + "mechanical_pass": true, + "failures": [] + }, + { + "row_index": 1, + "category": "room", + "id": "facility_00000005", + "layer": 1, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": true, + "text": null, + "local_rect": [ + 1027.0, + 388.0, + 256.0, + 192.0 + ], + "screenshot_rect": [ + 1106.0, + 485.0, + 256.0, + 192.0 + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "font_width": null, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54", + "path": "row-0001-image-0.png" + }, + { + "image_index": 1, + "sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54", + "path": "row-0001-image-1.png" + } + ], + "mechanical_pass": true, + "failures": [] + }, + { + "row_index": 2, + "category": "business", + "id": "0", + "layer": 1, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": false, + "text": "職員不足 未稼働", + "local_rect": [ + 133.0, + 390.0, + 79.0, + 14.0 + ], + "screenshot_rect": [ + 212.0, + 487.0, + 79.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 73.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae", + "path": "row-0002-image-0.png" + }, + { + "image_index": 1, + "sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae", + "path": "row-0002-image-1.png" + } + ], + "mechanical_pass": true, + "failures": [] + }, + { + "row_index": 3, + "category": "business", + "id": "1", + "layer": 1, + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "operation": "INACTIVE", + "dimmed": false, + "text": "職員不足 未稼働", + "local_rect": [ + 1029.0, + 390.0, + 79.0, + 14.0 + ], + "screenshot_rect": [ + 1108.0, + 487.0, + 79.0, + 14.0 + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "font_width": 73.0, + "eligibility": "visible", + "reason": "whole rectangle in viewport", + "crops": [ + { + "image_index": 0, + "sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869", + "path": "row-0003-image-0.png" + }, + { + "image_index": 1, + "sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869", + "path": "row-0003-image-1.png" + } + ], + "mechanical_pass": true, + "failures": [] + } + ], + "mechanical_pass": true, + "errors": [], + "unaudited_traffic_overlays": [], + "pending_categories": [ + "queue", + "EV", + "staff", + "goods" + ], + "pixel_categories": [], + "independent_review": "pending" + } + ], + "root_native_os_full_regression_independent_review": "pending" +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_BUDGET_REPAIR_SUBMISSION.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_BUDGET_REPAIR_SUBMISSION.md new file mode 100644 index 0000000..a82fd58 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_BUDGET_REPAIR_SUBMISSION.md @@ -0,0 +1,45 @@ +# R10 diagnostic budget repair submission + +Status: submitted for Root verification; no acceptance credit claimed. +Contract base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. +Starting submitted source: `336a8cd0b30db1add11869ebceab847db693f531`. + +The mirrored OPUS_R10_BUDGET_REPAIR_DIRECTION.json and +ROOT_OS_R10_BUDGET_FINDING.json resolve the input gap recorded in +R10_CONTEXT_GAP.md. That historical note and all copied evidence are retained. +The failed R10 B run remains 484 checks / 11 failures, validate +`4958d8f55b6b458694bf158769ce3bff`; it provides no acceptance credit. + +The observation fixture counted construction only. Its common A/B budget now +also counts all five planned candidates' registered catalog signing_cost and +the 40 RAW_FOOD purchase. The shared assignment list and purchase quantity +feed both budgeting and actual setup commands. PurchaseQuoteV2.make reads the +current SupplyCatalog unit_price; the service's eligibility branch selects +base_total or effective_total. The live supply preview is recorded and checked +against that budget immediately before purchase. No margin or production +value is added. Both budgets still include B's improvement construction. +The budget record retains construction rows and adds construction totals, +candidate sources/costs and signing total, item/unit price/quote/purchase total, +needed, initial resources and deltas. + +Every setup operation already passed through command(), which asserts its +CommandResult.is_success() and records its receipt. Failure names now include +the payload, distinguishing individual builds, hires and assignments. This +continues to cover conditions, purchases and kitchen production without skips. + +A cheap checker preflight verifies all three spend categories enter needed +before deltas and that the setup shares its assignment/quantity plan. Local +negative tests remove each spend increment and delta coverage. These are +static checks, not runtime evidence or acceptance assertions. + +Worker validation: 51 local Python checker tests passed; Python syntax checks +passed. No Godot, native, OS, full regression, Runner operation, git write, +production checkout access or additional agent was used. + +Root evidence for this repaired source is missing at submission, as expected. +Root must freeze the source and perform the exact native and normal OS A/B +proof, distinct-PID load, mandatory full177 regression, independent Opus review, +integration/final and ledger. Reuse requires matching applicability. The native +48-demand, observation cohort, seven conditions/six categories, staff/goods +coverage, 30/88 with no partial credit, OS child timeout 3600, worker/native +900 and null cap are unchanged. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_CONTEXT_GAP.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_CONTEXT_GAP.md new file mode 100644 index 0000000..6b4bf33 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/R10_CONTEXT_GAP.md @@ -0,0 +1,45 @@ +# R10 repair context gap + +Status: blocked on missing recorded R10 decision, not on missing Root proof. + +The read-only HEAD check returned the requested submitted starting point +`336a8cd0b30db1add11869ebceab847db693f531`. The contract base remains +`860136f7099f5e3aee99da5798a627a77f2d8eea`. The initial worktree contained an +untracked `tools/__pycache__/`; it was preserved. + +The supplied run direction requires keeping the recorded R10 budget repair +exactly narrow, but does not include that decision's concrete repair instructions +or location. The available `OPUS_CONCRETE_IMPLEMENTATION_DIRECTION.json` is the +initial recipe (run `83e836e232c74975aaf51f4fab5a1af9`), not the R10 decision. +No R10 decision was found in this checkout's P23 execution materials or fixtures; +`.review-material` is absent. A targeted JSON/Markdown search for R10 budget +direction in the sibling P23 runtime directory found no match. Access to the +normal production checkout's P23 execution directory and sibling validation +checkouts returned `Operation not permitted`; no access workaround was attempted. + +Static context inspected before considering source edits: + +- `API_AND_STATE_MAP.md` describes deterministic initialization and public + arrival/build commands. Current `simulation_runtime.gd:203` still accepts the + explicit seed, and `game_state.gd:20` resets resources to defaults. +- `layout_comparison.gd:21` declares the budget as `GameState.reset`; its run + resets state before initialization and records the initial snapshot before + issuing layout-specific build commands. +- `observation.gd:49` onward derives a common diagnostic construction budget + from registered definition costs, including the declared B improvement, and + issues `change_resources` before building. This is a separate observation path. +- `check_p23_life_observation_evidence.py:1212` requires equality of A/B initial + snapshots. These observations do not establish which precise repair R10 + authorized, or justify inventing budget/seed policy. + +Required continuation input: the complete recorded R10 budget repair decision +and its referenced failure evidence, mirrored into readable task materials (or +provided directly), including any prescribed budget/seed and checker changes. +No specification, acceptance, budget, production, fixture, checker, renderer, +or policy change was inferred. Only this context-gap note was added. + +No tests were run: there is no source repair to validate. No Godot, native, OS, +full regression, Runner operation, git write, integration, or additional agent +was used. Root exact-candidate native/OS/resume/full-regression evidence and +independent Opus review remain missing and required; no PASS or acceptance +credit is claimed. Root capacity recovery remains separate. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/R23_REPAIR_NOTES.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/R23_REPAIR_NOTES.md new file mode 100644 index 0000000..99c96c3 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/R23_REPAIR_NOTES.md @@ -0,0 +1,443 @@ +# P23-03 repair inspection + +## Repair 26 — submitted for Root validation; no acceptance credit + +This section supersedes the status below. Preserved the five premodified files, +both R13 fixtures, all 86 existing tests, and every historical section. Historical +49 and the twelve false R13 judgments are unchanged. Seven conditions, six +categories and 30/88 are unchanged. No Runtime, gameplay, save, economy, mana, +asset or UI-entry source was changed. + +Implemented the remaining local work: + +- Both independent packers now rebuild vertical components from fitted feet and + pose cores after every scale reduction. Scale ceilings decrease monotonically, + so termination has a finite measure and no iteration-limit exemption. Stable + components use the existing ordered forward/backward fitting and p=1 retained + member aggregation. A positive separates a previously intersecting component + at p=2; retaining the stale component would incorrectly reduce it to p=1. +- Whole-state fixtures cover queue lanes plus work staff, queue precedence and + order, the actual R13 cafeteria/access-cell geometry, carried and treated + patients, occupied rest facilities, queued staff, passenger/floor coexistence + on FRONT/BACK, and independently calculated no-fit aggregates. Negatives + include stale groups, pose/feet, carrier-relative positions, category/region/ + transit spoofing, missing identities and displaced aggregation badges. +- Cabin candidates no longer silently truncate after two seat rows. Additional + identities reuse the existing two-row anchor pattern and participate in the + same independent fit/no-fit decision. The synthetic saturation configuration + retains every boarded identity in member lists; it changes no definition. +- OS and offline callers use the shared hash-bound drawing_context loader. + Tests cover valid context, absent fields/objects, malformed raw/rest context, + report binding and object hash mismatches. Frame tests include valid transit + and occupied-rest authority, missing/malformed rest authority, and conservative + rejection of overlap without raw context. No flags grant exemptions. +- GDScript component fixture now includes four occupants, scale-driven regroup, + and actual _fit_life_actors aggregation with nine drawn and ten retained IDs, + badge text and position. This fixture has NOT been executed by Godot. + +Source/consumer audit: + +- _build_life_observation calls fitting before publishing; _draw_life_overlay + consumes fitted floor/passenger rows and the aggregation badge at/text. + _draw_observed_actor consumes at/scale/mode/frame/flip. Logical cell, preferred + center and state remain separate. Existing click handling selects world + facilities/construction/stairs; there is no actor hit-test to relocate. +- passive_observation.draw_rectangles and Python drawing_rectangle_rows consume + fitted actor extents and actual aggregation badge coordinates. frame_rows + carries all member identities and drawn_keys into review metadata. Whole typed + diagnostic captures preserve the same report; compare_actor_geometry rebuilds + classification, bounds, fit and aggregation independently. compare_rest invokes + complete geometry for occupied resting facilities. compare_motion binds the + actual published report/history, not unfitted preferred positions. +- inspect_offline_mirror and inspect_os_requests both pass drawing_context's + hash-bound state/rest objects to frame_rows. inspect_stream separately runs + rest, cabin, motion and complete geometry checks. Missing objects raise and + cannot become a successful exemption. Unit tests exercise this shared loader; + no real OS/offline evidence was rerun or relabeled. +- strict_test_markers.load_contracts rejects duplicate suite rows. The existing + single P23 row already declares the exact two source print sites' template + `P23 life observation: checks, failures`. parse_raw obtains + counts only from actual logs; source_sha256_at_schema_audit is historical + metadata, not a current-source acceptance bypass. No marker-contract append + is required, no suite-policy row was added, and no marker was fabricated. + Root's source map must bind the final modified fixture and all changed files. + +Local verification: 87, 91, 93, 95, 97, 98 and 99 tests passed as work progressed; +the final rerun passed **99 tests in 0.213s**, exit 0. The original expected R13 row11/29 rejection +is retained. Python compile checks for checker, tests and runner passed without +bytecode output. git diff --check passed. No tests were deleted or weakened. +Read-only discovery encountered inaccessible sibling worktrees and nonexistent +BUILD_OUTPUT_DIR shorthand; the actual allocated build directory supplied both +R13 direction files. A shell glob lookup had no match; no source was lost. + +Root native/OS evidence, the 177-test full regression and independent Opus +all-condition review remain missing, as expected for worker submission. +Submission is not acceptance and earns no unit credit. No Godot, native/OS, +full regression, Runner, provider, git write or agent was run. + +## Repair 25 continuation — current status: blocked, implementation incomplete + +This section supersedes the status of the historical sections below. There is +no specification, permission, or floor-interpretation gap. The remaining blocker +is unfinished implementation and verification, not missing Root evidence alone. +All 809 pre-existing added lines were preserved and extended. No candidate is +claimed implemented or ready for acceptance. Seven conditions, six categories, +and 30/88 remain exact; historical 49 is preserved without relabeling. + +Read before editing: AGENTS.md, Phase 0 API_AND_STATE_MAP, recorded concrete +direction, both authoritative R13 geometry/region directions, renderer, +checker/tests, diagnostic/passive collectors, Runtime initialize/spawn/save/load +entry points, enums, marker parser and existing contract. No new assets or +Runtime/gameplay/save/UI-entry changes. + +Changes in this continuation: + +- Drawing-row validation accepts the existing raw-state/rest-audit context and + invokes complete geometry classification and fitting. OS frames pass the + hash-bound stream objects; offline image review loads the matching writer + capture and checks its report binding before passing the same objects. +- Track validation checks the raw actor endpoint, source-valid adjacent origin, + finite nonfuture times and boolean pause. A drawing's region/in_transit flags + cannot grant an exemption. Without raw context the old conservative check + remains, and frame output explicitly says `absent: fail closed, no transit + exemptions`. Historical images have not been rerun or relabeled. +- Rest selection still independently validates routes, costs, locks and + eligibility. For actual occupied rest facilities its geometry now runs through + complete cross-kind fitting, including queue precedence and independently + reconstructed aggregation identities/no-fit, instead of requiring every + rest-slot identity to have an unshifted drawing row. +- Added the actual +m badge to Python and GDScript image-audit rectangle lists; + frame metadata retains all aggregate members and drawn keys. This additional + diagnostic row type does not change the six acceptance categories. +- Added full state-to-geometry positives for actual track interpolation, + staff/guest mixed packing on FRONT/BACK, fractional low-world-pixel scale and + saturated aggregation with all identities retained. Added track/time/pause, + forged-region/out-of-cell, aggregate identity loss and badge omission checks. + R13 row11/29 remains an expected FAIL. Existing tests were not removed. + +Precise outstanding TODOs before an implemented submission: + +1. Complete full state-to-geometry positives for queue lane + work staff, + passenger + floor, carried/resting/treated staff and cabin aggregation. + Exercise compare_rest with an actual occupied facility, legitimate no-fit + aggregate and queued staff; its new integration lacks those full fixtures. +2. Complete full semantic negatives for queue precedence/order and forcing the + receiving R13 guest back into its access cell. Existing primitive receiving + fixture tests and overlay negatives are retained but are not substitutes for + every required state-to-geometry case. +3. Audit and implement regrouping after scale changes in BOTH packers. They + currently form vertical groups before fitting; fitted feet/cores can change + vertical intersections. Do not claim the required regrouping positive passes. + Confirm carrier-relative pose/feet, cabin member coverage, and badge placement + through the required semantic cases before declaring the renderer finished. +4. Add full frame/offline/OS-context unit cases (including rest authority and + malformed/missing stream context). Standalone/full transit paths have tests; + production caller wiring was read but no OS/offline evidence run occurred. +5. Finish the GDScript UI fixture and source-authority integration audit. Existing + four-occupant UI preparation is retained and unexecuted. Marker contracts and + suite policies are unchanged; do not append a duplicate suite row or fabricate + an acceptance marker. If an authority append is necessary, only the single + strict contract-permitted entry is authorized. + +Local checks actually executed: + +- Checker unit command `PYTHONDONTWRITEBYTECODE=1 python3 + tools/test_p23_life_observation_evidence.py`: 78 tests OK (0.104s), 80 OK + (0.112s), 83 OK (0.134s), 85 OK (0.112s), then **86 OK (0.124s)**. + No checker unit failure occurred in this continuation. +- No-bytecode `compile(Path(p).read_bytes(), p, "exec")` syntax check for + checker, checker tests and Root runner: all three OK, exit 0. +- Read-only `git diff --check`: exit 0, no output. +- One shell heredoc edit attempt failed before execution (`can't create temp + file for here document: operation not permitted`); applied the intended edit + with apply_patch afterward. One read-only search named a nonexistent + `tools/regression/suite_policy.py`; read the actual JSON policy and runner + directly. These were tool/read failures, not test passes. + +No Godot/native/OS, full regression, Runner, provider, git write, integration or +other agent was run. Root's **new native/OS evidence**, the **177-test full +regression**, and the **independent Opus all-condition review** are still missing. +Those expected later proofs alone are not the reason for this blocked status. + +## Repair 24 continuation — current status: blocked, implementation incomplete + +The floor/anchored-region interpretation is RESOLVED. No clarification or +permission is requested. This status is an incomplete implementation, not an +external specification blocker and not a submitted candidate. Preserve this +worktree and every earlier uncommitted change. Historical sections below are +retained as history, not current interpretations. + +Read the current renderer, RoomPresentation, the recorded concrete direction, +both R13 direction files, Phase 0 API map, Runtime initialization/spawn/formal +save/load sources, and cleaning pose dependencies. No domain inputs or assets +were changed. Seven conditions, six categories and 30/88 remain exact. + +Implemented preparation in this continuation: + +- Added per-actor cell/track, lane, facility, work-column and carried-patient + bounds, inset by world pixel; added original logical cell to drawing rows. + Staff queue placement now precedes the special staff anchors. +- Added a cross-kind vertical grouping and forward/backward interval packer, + integer scale trials and deterministic retained-member aggregation with a + readable +m badge. Cabin rows participate. Preferred centers and pose fields + remain separate from fitted drawing coordinates. This GDScript has NOT been + executed and is not established as native-correct. +- Added independent Python interval fitting and replaced original-scale + assertions with reconstructed centers, bounds, poses, fitted rows and + aggregation comparison. Queue and room reports are cross-checked before + geometry classification; boarded keys are rebuilt from EV reports. +- Added tests for the 56px four-mover case (all p=2), mixed actor kinds, + saturation with all identities retained, individual intervals, and R13's + receiving cafeteria guest outside its unchanged access cell. Added a full + synthetic state-to-geometry four-mover case and mutations for cell, state, + scale, position, missing/duplicate actors and unnecessary aggregation. +- Added the four-occupant GDScript component fixture. It is NOT native evidence. + +Known remaining implementation work (must be resolved before submission): + +1. The full geometry entry point passes independently derived transit keys to + pairwise checking, but standalone drawing-row/offline checks still use the + conservative default with no transit exemptions. Reconcile those paths + using authoritative inputs; never trust an in_transit/region flag. +2. Existing compare_rest still expects each visible slot in actor_drawings and + its original rest center. Reconcile legitimate aggregation and the new + staff queue precedence without bypassing the independent no-fit check. +3. Finish full classification tests for queue/work mixed groups, queue + precedence/order, spoofed category, forced access-cell receiving position, + carried/resting/treated staff, passengers and FRONT/BACK. Existing primitive + packing positives are not a substitute for these full semantic cases. +4. Audit pose/feet and regrouping after scale changes, low-world-pixel views, + carried patients and cabin aggregation; validate the actual GDScript and + report consumers through Root only after worker readiness. No new native + correctness or complete collision-removal claim is made here. + +Checks actually executed in this continuation, all local: + +- `PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py`: + 71 tests OK (0.093s), 76 tests OK (0.102s), then **78 tests OK (0.111s)**. + Existing R13 row11/29 remains an expected rejection. No unit test failure + occurred during this continuation; earlier failures remain recorded below. +- No-bytecode `compile(Path(p).read_bytes(), p, "exec")` for checker, + checker tests and Root runner: all three syntax checks OK, exit 0. +- One dependency lookup used the wrong cleaning-bridge directory and returned + an rg missing-file error; located and read the actual source under + scripts/environment. This was a read failure, not a passing test. + +No Godot/native/OS, full regression, Runner, provider, git write or agent was +run. Root's **new native/OS evidence**, the **177-test full regression**, and +the **independent Opus all-condition review** are still missing. Historical +49 remains preserved and is not relabeled. No partial acceptance is claimed. + +The inspection below is retained history. The repair 24 section above records +the current status; the former full-box impossibility argument is superseded. + +Scope: the seven immutable acceptance conditions and existing allowed paths. +Validation: local Python syntax and checker unit tests only; no Godot execution. + +Read the immutable acceptance spec, recorded concrete implementation direction, +existing Phase 0 API map, renderer, actor art, and independent checker. +The R13 provider stdout, command, and prompt SHA256 values match 01-actual.json. +The actual structured review contains 8 frames and 132 rows. Its summary's +168-row claim is not used to replace the structured rows. False judgments remain +false. The reader-request-0003 whole screenshot confirms overlapping actors and +obscured traffic presentation. Historical runtime evidence is unchanged. + +Root native/OS evidence for a new candidate, the 177-test full regression, and +independent Opus all-condition review remain required and are not worker tests. + +## Status: blocked; partial independent checker preparation only + +The renderer repair and traffic geometry/layer audit are **not implemented**. +No claim of submission readiness or acceptance is made. No renderer, Runtime, +policy, immutable spec, source authority pin, or historical evidence was changed. + +### Placement specification gap and concrete failing evidence + +In the original `os-pair/A/reader-request-0003/request.json`, SHA256 +`7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c`, +the decoded report contains guests `os-p23-012` through `os-p23-015`, all +`MOVING`, logical cell `[1,1,0]`, with no service or queue request. Their +tracks' `from` and `cell` are both `[1,1,0]`. The current cell is 64x96 pixels +(`traffic` row 40's inset rect is `[709,390,60,92]`). Each actor's current +draw/report footprint is 48x54 pixels: the 16x18 PixelPeople grid at scale 3. +The four rectangles require area 10368, while the cell has area 6144. +Consequently offsets alone, preserving those sizes and containing all four +rectangles in that cell, cannot satisfy the requested nonoverlap invariant. +This is already present in the supplied evidence, not a hypothetical load. + +Source references: `world_preview.gd::_cell_dimensions`, `_actor_drawing`, +`_tracked_actor_position`; checker `drawing_rectangle_rows` and +`compare_actor_geometry`; `pixel_people.gd`'s 16x18 drawing grid. + +The direction specifies offsets inside room/cell art, containment consistent +with logical cell/state, retained motion, and no visual redesign. It does not +resolve what presentation change is permitted when existing footprints cannot +fit. Shrinking actors or expanding the permitted projection area could solve +this capacity issue, but neither is silently treated here as authorized by an +offset repair. This is a question about the admissible presentation, not a +claim that every possible rendering solution is impossible. + +Minimal proposal for the owner: specify whether deterministic scale reduction +within the original logical cell is permitted (including the required readable +minimum and behavior when it still cannot fit), or define an allowed room/lane +projection for these non-queued moving actors. No Runtime capacity change, +actor omission, or acceptance waiver is proposed. Retain all seven conditions. + +### Independent work preserved + +Added `validate_actor_nonoverlap` to both the semantic geometry path and offline +drawing-row gate. It recomputes full footprints from raw `at`/`scale`, includes +cabin passengers, rejects duplicate identities and invalid numeric geometry, +and does not use eligibility labels or asserted booleans. Edge contact is +allowed; positive-area overlap is rejected with actor identities. + +Five added unit tests cover edge contact, overlap despite forged verdicts and +off-screen labels, cabin/floor overlap, invalid footprints, and the actual R13 +false row 11 against staff row 29. The small historical-negative fixture copies +only the needed raw drawing inputs and preserves `[11,false,false,false]` with +request/manifest hashes. Its inputs were checked against the original typed +report. The original review files and all their rows remain untouched. + +The original manifest hash matches +`53aa8e2f12e5d0a089529457fcd55ee1ba0600e8dffff2225bb8b057d007fe1a`; +all crops in that manifest pass SHA verification. Opened the whole decoded image +and crops 11, 29, 40 directly; crops 11 and 29 show the same covered figure. + +Still missing: collision-free renderer placement, draw-consumed band rectangle, +actual layer/geometry audits, and the requested additional cell/state, band, +layer, and stale-frame negatives. Passing these local tests does not resolve +those missing deliverables. + +### Local checks actually run + +Before edits: +`PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py` +returned exit 0, `Ran 51 tests in 0.142s`, `OK`. + +After edits the same command returned exit 0, +`Ran 56 tests in 0.093s`, `OK`. + +Syntax command (no bytecode output): +``` +python3 -c 'from pathlib import Path; files=["tools/check_p23_life_observation_evidence.py","tools/test_p23_life_observation_evidence.py","tools/run_p23_life_observation.py"]; [(compile(Path(p).read_bytes(),p,"exec"),print("syntax OK:",p)) for p in files]' +``` +Exit 0; printed `syntax OK:` for each of those three files. +`git diff --check` returned exit 0 with no output (read-only check). +No Godot, native/OS, full regression, provider, Runner, git write, or agent +execution occurred. The initial shell heredoc attempt failed before execution +because the shell could not create its temporary file; subsequent inspection +used `python3 -c`. + +## Recorded geometry direction continuation — blocked pending placement clarification + +Read the complete `BUILD_OUTPUT_DIR/negative-image-review-r13/ +OPUS_GEOMETRY_RESOLUTION_DIRECTION.json`, the owner run direction, the current +PixelPeople and WorldPreview sources, RoomPresentation slots, the existing +Phase 0 audit and the actual initialize/spawn/save/load API sources. The spec +still matches its contracted **stripped-text** SHA256. No spec, Runtime, save, +asset, UI entry, gameplay value, authority entry or suite policy was changed. + +### Implemented independent preparation + +- Replaced the full-box overlap invariant with identity cores. The table is + pinned to PixelPeople SHA256 + `a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c`. + Upright walking/carry-walking phases 2 and 6 shift the core up one grid unit. + Standard-room sleep phases 2, 3 and 4 extend its top from y=11 to y=10, + keeping the bottom at y=16, as required by the actual breathing marks. + Flips use the actual 16-grid mirror. Missing pose inputs now fail closed. +- Preserved the edge, forged-verdict, cabin, invalid-geometry and R13 tests. + Edge coordinates now use 6x12 core extents instead of the rejected 16x18 + full-box extents; the deliberate overlap moves from [15,17] to [5,11]. + Added a passing full-box-overlap/disjoint-core case and bob, breathing, + flip, missing-pose, duplicate, forged-transit and source-pin tests. + R13 row11/29 still fails: both recomputed cores are [669,520,18,36]. + The fixture's newly required mode/frame/flip values were copied from the + original request and checked against it. False judgments remain false. +- Added the LifeOverlay Node2D between InactiveLayerRooms and LayerLens, + connected its draw signal, and queued its redraw from the parent draw. + The overlay draws traffic first, then cabin passengers and floor actors. + Existing inactive-art alpha remains 0.22. Actor rows and animation inputs + are unchanged. The report records observation/consumed serials, actual + child indices and the passes executed in the callback. +- Added the prescribed band rectangle at cell_bottom - 2*world_pixel, + with existing thickness clamp, colour thresholds and 0.90 opacity. + The blocked X is contained in the band pass. `compare_traffic` independently + recomputes both cell and band rectangles from typed state. Overlay checks + reject stale serial, reversed passes, child-order errors, a core covered + by the band, and forged band geometry. Source wiring/order is also checked, + so valid report flags do not excuse the tested reversed source callback. + FRONT/BACK local synthetic checks and GDScript serial/order assertions were + added. GDScript was **not executed**. + +### Exact outstanding interpretation + +Placement rule 1 gives service/rest/treatment anchors a facility region, while +rule 2 says a floor actor must *also* remain in its own logical cell. It is +unclear whether “floor actor” excludes these facility-anchored actors or means +every non-passenger. This is consequential in the actual R13 request, not only +in a fabricated layout: + +- `guest-auto-00000044` receives `service_00000009` at cafeteria + `facility_00000003`. Its unchanged logical cell is [5,0,0]. The recorded + traffic inset [965,486,60,92] reconstructs cell [963,484,64,96]. + The facility rect is [1027,388,256,192], and its four-pixel inset starts at + x=1031. There is no common x interval with the logical cell ending x=1027. +- `guest-auto-00000019` receives `service_00000002` at standard room + `facility_00000007`, rect [131,292,128,96]. Its logical access cell [-6,2,0] + reconstructs to [259,292,64,96]. The facility inset ends x=255; again there + is no common x interval. Its room pose is standard_room_sleep. + +`r13_service_region_gap.json` preserves the needed original report fields, +with source request SHA256 +`7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c`. +Direct comparison verified every copied guest, traffic and room rectangle. +A diagnostic unit test demonstrates the empty intersection for both examples. +These extracted fields are not substituted for whole typed acceptance evidence. + +This does **not** establish that the new specification is impossible: rule 7 +allows aggregation when p=1 cannot fit. If floor means all non-passengers, these +otherwise roomy facilities must aggregate their service guests with zero drawn +members. If floor means unanchored actors, those guests can be packed and drawn +in their facility region. The initial commentary's “constraint conflict” is +therefore narrowed to this interpretation question. No cell restriction was +silently removed and no zero-drawn service aggregation was silently adopted. +An asynchronous question asks the owner to select the intended interpretation. + +### Remaining implementation and evidence + +Ordered placement, scale fitting, no-fit aggregation, queue-order preservation, +carrier-relative fitted placement, transit recomputation/exemptions and their +checker integration remain unimplemented. The checker remains conservative: +an asserted in_transit flag never bypasses overlap, but the required genuine +transit exception is not implemented. The existing geometry checker still +expects original actor positions/scales. The four-occupant GDScript fixture, +positive packing cases (staff/guest, passenger/floor, queue, saturation and +carried patient), and the new containment/aggregation/queue-order negatives +remain outstanding. The partial renderer changes are not a submitted candidate +and are not claimed to solve R13 actor collisions. All existing tests remain. + +Still missing: Root **new native/OS evidence**, the **177-test full regression**, +and **independent Opus all-condition review** for the final candidate. +Historical 49 is preserved and is not relabeled or credited to these edits. +Seven immutable conditions and all6/30/88 remain exact, without partial credit. + +### Checks actually run in this continuation + +- `PYTHONDONTWRITEBYTECODE=1 python3 tools/test_p23_life_observation_evidence.py`: + first core run: 63 tests, 7 failures and 1 error because older synthetic + reports omitted pose fields. Updated those fixtures with explicit poses; + 63 tests then passed (0.089s), followed by 64 (0.094s) after the bounds + diagnostic. First overlay run: 71 tests, 1 error because the older spatial + fixture lacked the new band rectangle. Updated that fixture with explicit + band geometry and consumed-frame fields. Final run: **71 tests, OK, 0.081s**. +- The existing no-bytecode `compile(Path(p).read_bytes(), p, "exec")` syntax + check passed for checker, checker tests and Root runner script (exit 0). +- Direct Python read-only comparisons verified both extracted R13 fixtures + against the original SHA-bound request and verified the immutable spec pin. +- A shell heredoc read attempt failed before execution with “can't create + temp file”; inspection continued with `python3 -c`. + +No Godot, native/OS, full regression, Runner, nested provider, git write or +other-agent execution occurred. Only allowed files were edited. The worktree's +previous uncommitted work was retained and extended. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_CONTINUATION_HANDBACK.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_CONTINUATION_HANDBACK.md new file mode 100644 index 0000000..1a515bf --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_CONTINUATION_HANDBACK.md @@ -0,0 +1,86 @@ +# Repair28 continuation / repair29 source handback + +Record: SOURCE-READY, source-only submission for P23-03. This is not acceptance. +The preserved repair28 implementation is unchanged by this continuation. Only the +existing P23 marker source audit SHA and this new note are changed in repair29. +Older notes, raw receipts and failure history remain unchanged. + +## Input and preserved-state verification + +Read all six Root inputs in full from the explicitly supplied +`.runtime/P23-03/build/audit-support/root-repair29-input/` directory outside W. +Each byte count and SHA256 matches MANIFEST.json. Its authoritative patch is +64,737 bytes (SHA256 6b0a2a236a08f1c473850c06f7a761d1f0f8f4749db271771550d409d46e37b0), +despite the older instruction's 384318-byte description. No input is missing. +Before editing, all eight W file lengths and SHA256 values matched +PRESERVED_MANIFEST.json; `git diff --binary` equalled PRESERVED_DIFF.patch byte +for byte. The untracked r15 fixture was separately verified by its manifest SHA. + +## A–E disposition and source map + +| Item | Existing implementation / disposition | +| --- | --- | +| A1–A2 | scenes/main/world_preview.gd:1057 expands stops by reported shaft width; :1076 stops outward lanes at the footprint; :1094 retains member/visible/overflow accounting and badge offset. tools/check_p23_life_observation_evidence.py:965 independently reconstructs queues. | +| A3 | scripts/ui/room_presentation.gd:77 supplies count, operation, doors and phase text; scenes/main/world_preview.gd:1224 draws existing PixelArt cabin/gate and badge on LifeOverlay; :811 draws cabins above inactive-layer context. | +| A4 | scenes/main/world_preview.gd:640 collects opaque obstacles, :674 subtracts rectangles, :831 draws every traffic row including fully covered rows beneath opaque later cabins/badges. No density row is discarded. | +| A5 | scenes/main/world_preview.gd:564 deep-copies the consumed model, including shaft_footprint (:1097), badge_rect (:1286), traffic_obstacles (:649), segment_rects (:665). | +| B(a–d) | tests/ui/test_p23_life_observation.gd:32 covers width-2, overflow, empty cabin, both layers, operation/doors/phase and traffic geometry. tools/test_p23_life_observation_evidence.py:40 adds meaningful R15 intrusion/traffic and typed-state negatives. tests/fixtures/p23-03/passive_observation.gd:194 uses the reproducible badge rectangle. | +| C1–C2 | tools/check_p23_life_observation_evidence.py:1840 validates external S/M/T/Q, allowed changes, deterministic merge tree, S-only/M-only blobs and complete T source/settings/input/QA bindings; :1799 and :1821 enforce the structural marker join retaining M/P18. tools/run_p23_life_observation.py:245 consumes external roles; :334 records v2 evidence. tools/test_p23_life_observation_evidence.py:135 covers composition negatives. | +| C2(vi), C3 | tools/check_p23_life_observation_evidence.py:2108 refuses v1 history as current evidence; :1986 allows one screenshot only for screenshot requests, retaining real-input pairs. Existing staff, OS receipt, distinct-PID resume and image gates remain. Root owns final full regression and independent review. | +| D | The earlier assertion that kitchen full177 was consistent is withdrawn: p04-final-product-full-r01 failed the relationships-v2 kitchen envelope (legacy/version/original_relationships added; threshold_candidates/next_candidate_sequence missing). Old 177 gives no T credit. No kitchen source or overlay was changed. ACTUAL_SUPPORT_COMPLETED.json records approved QA3a9c support integration 5041b88bdf25d0deaf3cb335be981896522c4880 (reported main tree df9b2851edaa7d02a55b834328cf9e9a9bb7203a). Root must bind the actual later M and its kitchen QA blob to T; neither 9b nor any old full result is fixed as authority. The four D05 choices are not worker blockers. | +| E; repair29(b–c) | This note supplies the continuation handback and structured SOURCE-READY record. | +| repair29(a), step 4 | completion_marker_contracts.json:3710 now pins the actual final GDScript test SHA. Only that existing row's audit SHA changes; template, required flag, authority scope and every other row remain intact. | + +No concrete remaining A–C source omission was identified in this bounded check; +therefore no code or test was changed and no new negative test was needed. + +## R15 negative fixture provenance + +tests/fixtures/p23-03/repair28_r15_negative.json is an extraction of historical +local-coordinate fields, not a raw receipt. It identifies source 9a26f9a, +manifest a25e15f00d09acb16b05e180a6263c3771c84449bd6e0c99b6968fd739727bd2, +request 7099ca03da3cef5819e7965f6fdea8777b9995a9e12806e870e844ec2b02b9a5, +R15 os-pair/A/reader-request-0003/request.json and negative rows 6/17. +Tests calculate counterfactual geometry only in memory. This extraction and all +synthetic projections grant no S/T execution or OS credit and do not relabel raw history. + +## Frozen changed-source SHA256 + +| Path | SHA256 | +| --- | --- | +| scenes/main/world_preview.gd | 6c43e19fbce8ff5c442017ab22443f39f65f356c308ee895355cd7678d307f33 | +| scripts/ui/room_presentation.gd | 2eda851534777575298ea005e2e6d0ee90785a81357d5b9c34cfb95f615a8181 | +| tests/fixtures/p23-03/passive_observation.gd | 812c2a3b399b079333e4eae0fbf78946f1c39041e6859e65f8bc7c44aaad6bfc | +| tests/fixtures/p23-03/repair28_r15_negative.json | 8525cdfbc504583778624ab103efb4642374715be73187e97bdbf18d180a1616 | +| tests/ui/test_p23_life_observation.gd | 610ff2f33ec0e42bdca55d692e640c9bd8fe9244b9b49b0263419b1998ff9e9e | +| tools/check_p23_life_observation_evidence.py | 767f3452cd9f0671e25bfda113153b0b1471fcbfdfa7aba6133f991a61cf306d | +| tools/run_p23_life_observation.py | 1fa85aa53886887f9611997194240bf6e112f831e8f36ea3aff982d12538bdec | +| tools/test_p23_life_observation_evidence.py | 4a52215b125f3445abde9992ee69ad9b09fc114977e359b52f190c966e6d3c39 | + +## Verification and pending Root evidence + +ROOT_LOCAL_TEST_RESULT.json is authoritative: `python3 -B +tools/test_p23_life_observation_evidence.py`, exit 0, **123 tests, 0 failures**, +0.267 seconds. The earlier worker count of 121 is superseded. These unchanged +tests were not rerun in repair29; no Python source changed, so no syntax rerun +was needed. Manifest/hash/diff comparisons via `python3 -B -c` exited 0. +`git diff --check` exited 0. Final `python3 -B -c` hash/byte comparison exited 0: +all eight preserved sources remain unchanged, and the marker file differs from +HEAD by exactly the one authorized SHA replacement. Marker file SHA256 is +4dbba39cad454d02a004ee3d000457c3e13f7fc017dcfdb847aca7b192a8e30b. +The P23 marker entry SHA256 is +f83f022c44db6f89fc18d634f7ec9ce54b56450299c0e30fb1275cd2b88b6b39 +(UTF-8 JSON, sorted keys, compact separators, ensure_ascii=False). +Initial shell attempts failed before validation: heredoc temporary-file creation +was denied (exit 1); `python` was unavailable (exit 127). The successful checks +used `python3 -B -c` without temporary files. + +Still missing and explicitly Root's work: exact-source native/component and +impact evidence, same-demand A/B, normal OS on both layouts across all six +categories and FRONT/BACK/pan/zoom, formal save and distinct-PID normal load, +actual CUA receipts and independent bound-frame image judgments, one mandatory +full regression on T, and independent Opus all-condition review. Root binds +S/M/T/Q after submission and performs normal integration/final gates. No worker +engine, OS, Runner, Git write, main operation or subagent was used. Seven +conditions, six categories, 30/88 denominator, formal13, max repairs null and +all counters/caps remain unchanged. No acceptance verdict is issued. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_TRAFFIC_PROJECTION.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_TRAFFIC_PROJECTION.md new file mode 100644 index 0000000..cddc75d --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_TRAFFIC_PROJECTION.md @@ -0,0 +1,26 @@ +# Repair 28: traffic projection checker + +Scope: checker, its Python tests, one minimal decoded fixture and this note. Renderer, Runtime, save paths and controller unchanged. No git writes, Godot/native/OS, Runner or full regression operations. + +## Separate routing and defect records + +- Normal negative validate 97b6cdf9 failed because the current index was absent. Historical 49 was preserved by canonical rename retaining index SHA c0cc9e…. That missing-current-proof result is not evidence of this static defect. +- Independent readiness 887f9684 (READINESS_19FC3F5_ACTUAL.json) identifies the actual checker overconstraint. ROOT_19FC3F5_BAND_MISMATCH_REPRO.json and OPUS_19FC3F5_BAND_SCOPE_DIRECTION.json describe its repro and authorized scope. + +All seven repair27 input files were read/decoded directly and their byte counts and SHA256 checked against MANIFEST.json (original paths retained in fixture provenance). The original frame-180 landing densities remain zero. The new 45 KB fixture contains only the unchanged decoded fields used by overlay validation: report frame/source metadata, actor drawings, cabins, traffic and camera/elevator track fields; raw world bounds, elevators and staff tasks. It is an extraction, not a raw receipt. No unused megabytes of state, rest or motion were copied. The positive test is explicitly COUNTERFACTUAL: only its in-memory report densities at (0,1,0) and (1,1,0) become 1, covering guests 018/019. It grants no engine/OS acceptance. + +## Correction and caller audit + +compare_traffic passes its actual snapshot into validate_life_overlay(report, root=ROOT, snapshot=None). inspect_stream already binds that state to its raw object and invokes compare_traffic; native layout and OS writer/reader stream validation therefore receive raw authority. The controller run_p23_life_observation.py has no direct validate_life_overlay/compare_traffic caller. Offline/OS drawing paths already use drawing_context to bind report/state/rest and retain compare_rest, compare_cabin_geometry and compare_actor_geometry. No new drawing gate was added there. Existing direct unit callers without state remain fail closed. + +Cabin exemptions apply only to passenger entries belonging to that same raw shaft; cabin geometry reuses compare_cabin_geometry. A floor entry cannot inherit a passenger exemption by copying its key. Carried exemptions require actual active rescue transport binding and reuse compare_actor_geometry for original binding, track and raised-region fitting, excluding boarded/queue placements. Existing rest-authority checks remain unchanged. Missing-carrier KeyError remains deferred and fails closed. + +Serial, consumed serial, reported passes, source child/pass order, source connection/parent restrictions, band rectangle and same-report checks remain unchanged. Floor cores keep the same strict intersection predicate. Every intersecting actor contributes a clipped horizontal interval. Sorted interval union must leave a nonzero uncovered span; no ratio or threshold was introduced. + +## Local verification + +- Python 3 -B compile() of both changed Python files: passed without writing bytecode. +- Entire checker suite: 108 tests passed (all 99 existing plus 9 new); original R13 row11/29 negative retained. New tests cover counterfactual cabin success, occupied and blocked full union occlusion, spoofed cabin rectangle/membership, absent raw state, floor copy of cabin key, missing carrier binding, and carried binding/track/position mutations. Existing reversed report/source pass tests remain. +- git diff --check: passed. + +Root native/component, A/B on both layouts, normal OS, one full177 and independent Opus all-condition pixel review are expected after submission and do not block worker submission. No such unexecuted checks are claimed passed. Registered timeout 900 s, seven conditions, six categories, 30/88 and formal credit 0 remain unchanged. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR35_SOURCE_READY.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR35_SOURCE_READY.md new file mode 100644 index 0000000..80590ee --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR35_SOURCE_READY.md @@ -0,0 +1,90 @@ +# Repair 35 — source submission + +Scope: P23-03 cabin-intrusion presentation repair only. Source input is +`93248f4d79a3be71d7a1ac0592e9219d405cd6b7`; recorded tested Q is +`428f653cdac375c1d4589e6c2d6138e914acf9da`. No Runtime, demand, assets, +acceptance, policy, process ownership or timeout changes. + +## Exact inputs and Phase 0 + +First read the repair35 `EXACT_INPUTS.json` under +`/Users/korizatou114/Projects/TheTree-managed-worktrees/p23-03-code/.runtime/P23-03/build/audit-support/cabin-repair35-exact-inputs/`. +Read its complete supplied `original_full_request.txt`, `actual_Opus_repair.json` +and `actual_Opus_run.json`; all three supplied SHA256 pins match. Provenance-only +source locations were not needed. Read the sibling actual-worker preflight. + +Read the SHA-verified preserved all87 probe +`audit-support/cabin-repair34-exact-inputs/CURRENT932_ALL_CABIN_INTRUSION_PROBE.json` +under the same BUILD (SHA256 +`da395d697e0ec1bd2fb805757aa320314bbc840260f6062d12ecf5d859ceebd0`). +Read all 87 complete collision rows and all their whole SHA-verified typed +state/report objects: 19 distinct stream/state/report pairs, 31 distinct objects. +Also decoded all referenced objects in all 161 current stream captures and read +every file in the supplied r16 stage (1962 files, including the actual Q sources). +No required evidence was truncated, compressed, relabelled or edited. + +Used that preserved evidence to apply the new candidate-region rule: 70 rows +select a corridor landing and 17 require identity aggregation because no lane is +available. All 87 candidate regions intersect a cabin; outside-class rows: zero. +This is static application to old negative evidence, not new rendered proof. + +The existing `API_AND_STATE_MAP.md` remains the deterministic-demand/save-load +audit. Directly checked current GuestRuntime enum (`MOVING` = 3, lines 6–19), +WorldService `get_occupant`/`get_entity_cells`/`is_walkable` (194/204/228), and +ElevatorService's copied public report (345). These read-only APIs supply the +same queue lane topology and full-width shaft geometry; no new API is needed. +Current complete dependency SHA256: + +| Source | SHA256 | +| --- | --- | +| scripts/world/world_service.gd | add67a0799f961a7dce1d382ea6dac67022fbd013f8c4c6b11321df96f37944a | +| scripts/transport/elevator_service.gd | cea09dd9204551c26b31b30cd3aa847cc77e3b75fdac31662b9694a95fb3580c | +| scripts/guest/guest_runtime.gd | 57ba99e5d977b5302987f4359a19056f1ab09c8fb7e4762ee8d79cbb9316741a | + +The immutable spec matches `8568457d…a9c89f` using the existing checker's +`read_text().strip()` hash convention. Original acceptance and recorded complete +Opus repair/run directions remain authoritative. + +## Six pose classes + +| Case | Product, world_preview.gd | Independent checker | +| --- | --- | --- | +| (a) MOVING/idle footprint actor | 1608 candidate region, 1651 cabin rule, 1743 center before fitting | 1532 candidate region, 1577 cabin rule, 1584 center before jitter | +| (b) Lift-to-corridor walking transit | 1555 merged track bounds, 1565 landing becomes non-transit | 1532 merged track bounds, 1458 landing becomes non-transit | +| (c) Carrier and raised patient | 1624 recursively relocated carrier region, then minus 12p | 1556 recursively relocated carrier region, then minus 12p; 1612 center | +| (d) Fractional cabin crossing adjacent floor | 1565 positive-area intersection with actual cabin rect | 1458 intersection after independent cabin/badge validation at 1505 | +| (e) No corridor lane | 1590 landing badge anchor, 1759 aggregation with every identity | 1482 landing badge anchor, 1703 identity aggregation | +| (f) Queue plus relocated actor | 1602 prefer unreserved lane, otherwise existing packing | 1487 reservation choice, 1712 existing packing | + +Room slots, queue placement/overflow, cabin/passenger geometry and overlay order +are retained. The new landing centers preserve original jitter; carried patients +use the relocated carrier before the 12p raise. No-lane actors share a counted +aggregation at the floor's landing badge anchor, retaining key/id/state/cell/mode. +Report-only region/boarded assertions cannot authorize a position. + +`validate_cabin_intrusion`, `validate_actor_nonoverlap`, `actor_track_transit` +are byte-identical to the supplied Q checker; `PIXEL_PEOPLE_CORE_SHA256` is +unchanged. The existing P23-03 marker source-audit SHA alone was updated; all +other marker rows and suite-policy are preserved. + +`run_p23_life_observation.py:315` inspects both completed headless layout streams +inside the existing exception/receipt/cleanup path, before OS collection. +Failure therefore prevents OS collection. This is staging, not OS evidence. + +## Verification and remaining Root work + +- `py_compile` passes for the runner, checker and checker tests. +- The full Python checker test file passes: 142 tests, including the seven new + tests covering (a)–(f), old in-cabin positions, tampered flags, missing actors, + missing aggregation identities, combined no-lane identities and unwarranted + relocation. Existing 135 tests are retained. +- Native assertions were extended at `tests/ui/test_p23_life_observation.gd:56` + for same-floor and fractional moving cabins on both layers; not executed here. +- No Godot, native/OS collection, full regression, Git writes, Runner operations + or subagents were run. Root native/OS evidence is missing. + +Root staging remains: affected native/UI test; typed headless A/B with early +strict inspection and new-output intrusion probe requiring zero; then current +S/M/T/Q OS r17; then one final full 179 and independent review. Keep all r16 +negative inputs and the all87 probe versioned. Seven conditions, +30/88/formal-13 and no partial credit remain unchanged. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md new file mode 100644 index 0000000..a5d2993 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md @@ -0,0 +1,69 @@ +# Repair36 worker submission + +Scope: world_preview observation labels, independent checker, Python/GD controls. +No Runtime, service, data, art, policy, original negative, or acceptance changes. +Input authority: registered BUILD `audit-support/badge-repair36-exact-inputs/EXACT_INPUTS.json`, source 26f60095; original paths are provenance identifiers. + +The resolver places fixed cabin rectangles/badges first, then business, supply, +landing, aggregation in array order. A positive-area collision advances to the +first blocking rectangle's right edge and repeats. With finite rectangles of +positive width this always terminates on the anchor row; the specified upper +and lower fallback rows are therefore unreachable. No margin, pixel threshold, +clipping, truncation, member alteration, or new viewport rule is introduced. +Original anchors and ordered blocker identities are retained in deep-copy reports. +Traffic obstacles use the resolved positions; actor placement/traffic subtraction +and cabin/transit guards retain their existing implementations. + +The checker separately projects anchors back into the original report shape for +existing raw-state queue/business/aggregation recomputation. It independently +replays label geometry and validates reported positions and blocker identities. +All label pairs must have zero positive-area intersection. Both production image +review consumers require explicit indexed `judgments`; the legacy `rows` helper +interface remains available for unchanged historical unit controls only. Compact +bits are never accepted for new image-review evidence. + +## Static all40 comparison + +Inputs: all40 original manifests plus their unchanged original requests in +`p23-normal-os-26f-r17`, read only. This is a counterfactual static projection, +not a new rendered screenshot or native/OS acceptance. + +Original manifest label pairs: exactly one intersection, B/writer-request-0021, +rows 5 (queue) and 26 (aggregation). Resolved labels: zero intersections in all +40 frames, including fixed cabin labels. Thirty frames have no movement. +The following is the complete movement list in local canvas coordinates: + +| Frame | Label | Original | Resolved | displaced_by | +| --- | --- | --- | --- | --- | +| A/reader-request-0003 | landing/0 | 563,468 | 755,468 | cabin/0/rect | +| A/writer-request-0014 | landing/0 | 563,468 | 845,468 | cabin/0/badge | +| A/writer-request-0021 | landing/0 | 563,468 | 875,468 | cabin/0/badge | +| A/writer-request-0025 | landing/0 | 563,468 | 855,468 | cabin/0/badge | +| A/writer-request-0030 | landing/0 | 563,468 | 755,468 | cabin/0/rect | +| B/reader-request-0003 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge | +| B/writer-request-0014 | landing/0 | 563,468 | 1101,468 | cabin/0/badge, cabin/1/badge | +| B/writer-request-0014 | landing/1 | 819,468 | 1217,468 | cabin/1/badge, landing/0 | +| B/writer-request-0021 | landing/0 | 563,468 | 755,468 | cabin/0/rect | +| B/writer-request-0021 | landing/1 | 819,468 | 1011,468 | cabin/1/rect | +| B/writer-request-0021 | aggregation/0 | 563,372 | 825,372 | landing/2, cabin/0/badge | +| B/writer-request-0025 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge | +| B/writer-request-0030 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge | + +The affected queue anchor remains local [563,372], corresponding to original +manifest screenshot [642,469]. Its five members remain hidden and its complete +label remains unchanged. The historical reviewer index24/text26 disagreement +and all prior negatives remain untouched; no staff defect or acceptance credit +is inferred. + +## Validation boundary + +Worker runs Python syntax/local semantic tests only. GD controls are added but +not executed here. Root still must replay currentbad, run localized/native, +new normal OS A/B with indexed independent image review, then one full regression +and formal review/integration/ledger in the recorded order. No formal acceptance, +original88 credit, Windows, held-gesture, performance, or asset credit is claimed. + +Local result: Python AST syntax PASS; 146 Python tests PASS (142 retained plus +four focused controls). All 142 pre-existing test method ASTs match HEAD exactly. +`git diff --check` PASS. Only the existing P23 GD marker source SHA was refreshed; +no marker requirements or policies changed. No Godot/native/OS/full was run. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR37_PORTABLE_FIXTURE.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR37_PORTABLE_FIXTURE.md new file mode 100644 index 0000000..d447291 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR37_PORTABLE_FIXTURE.md @@ -0,0 +1,21 @@ +# Repair37 portable recorded negative fixture + +Candidate starts at `059c2f743be655c11627a2b19d8eab9f86a000a6` under the unchanged P23-03 contract. No Git write or Runner operation was performed. + +`tests/fixtures/p23-03/recorded-badge-negative.json.gz` is copied byte-for-byte from Root's `.runtime/P23-03/build/audit-support/badge-repair37-portable-fixture/PROPOSED_PORTABLE_FIXTURE.json.gz` in the parent worktree directory. SHA256: `17b1596fb1dca0436cd656ac21a9dc754c75b4485335a0aebe533f70e1c1b5ac`; gzip length 83,518 bytes; decompressed length 3,490,211 bytes. Repeated decompression yields identical bytes. + +The bundle records observed source `26f60095bff015efbf7d7cf89b5b0bbab1fec31e`, observed tested commit `dfe0dc44b0b88ace598c1b8bd05283c6084cb081`, and original PID 87103. These are preserved negative-input provenance, not acceptance evidence for 059 or this candidate. Original relative paths below are relative to the preserved `p23-normal-os-26f-r17` evidence root: + +| Payload | Original relative path | Bytes | SHA256 | +| --- | --- | ---: | --- | +| request | `os-pair/B/writer-request-0021/request.json` | 655285 | `f9cd2d5ff9dd63d9cf7ae4a295ccef8eea16f75ac784696dc40906703731e1d8` | +| state | `os-pair/B/writer-stream/objects/724d8fe362ba3e05eddd83c4b7c54da101899df6cd91ae88250feea3d4feff82.json` | 1867011 | `724d8fe362ba3e05eddd83c4b7c54da101899df6cd91ae88250feea3d4feff82` | +| manifest | `os-pair/B/writer-request-0021/review-image-0/manifest.json` | 75726 | `a98782ec1263aba400d542862dd084d2f1dd4cf7a0851cf7f0679867e545a338` | + +The loader verifies all three payload lengths and hashes on every call. Request uses JSON, report and state use the existing typed decoder; state retains the whole-Dictionary check. There is no runtime lookup, fallback, cache, extraction, or skip. + +Local validation: all 146 Python tests pass; AST audit against 059c2f7 confirms 145 test methods are source-byte and AST identical, and the remaining method is AST identical after replacing only the manifest assignment value with the same fixed placeholder. Python syntax compilation passes. A fresh isolated copy of Git-tracked candidate files plus the fixture, without `.runtime`, passes Repair36LabelTests 4/4. Separate corrupt-payload and missing-fixture controls fail with fatal errors. No extra fixture is retained. + +Frozen whole-file SHA256 values remain unchanged: renderer `4bc49a19a38b9c3b4d6bfc243ddf46f1f25469a8273adb5e812911ef9424a1f2`; checker `e41479ae51bccdfab3f31bd348794b441c2f341179bbe33222e2aacdf88f987f`. + +Native and OS checks are still pending. Root's final local run on the new tested commit, raw geometry/native checks, new normal OS run, every indexed-image review, mandatory full 179, formal all 7, integration and ledger remain pending. No Godot or full suite was run by this worker. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md new file mode 100644 index 0000000..0303b76 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md @@ -0,0 +1,296 @@ +# P23-03 worker status — static-complete submission + +Contract base: `860136f7099f5e3aee99da5798a627a77f2d8eea`. +This finalization preserves every pre-existing modified and untracked file. +Static-complete is supported by the wiring inspection and local checks below. +No acceptance claim is made. No Godot, native, OS collector, Runner, full +regression, git write, branch change, or other worker was used. + +## Current wiring confirmed by git diff and rg only + +Line numbers refer to the current candidate. + +- Checker `tools/check_p23_life_observation_evidence.py`: `check` at 1189 + reaches `inspect_native_phases` (1162) at 1215 and `inspect_stream` (950) + at 1220/1226. The stream inspection invokes `compare_rest` (531) at 968, + `compare_motion` (738) at 974, and `compare_actor_geometry` (800) at 975, + alongside actor, room, queue, traffic, goods, badge and cabin checks. +- `check` reaches `inspect_os_receipt` (1038), `inspect_os_requests` (1092), + and `inspect_resume` (1070) at 1249, 1250 and 1257. + Request inspection calls `screenshot_pixels` (184) at 1143, which invokes + `inspect_conversion` (158) at 191 for JPEG evidence; rectangle/pixel + checking follows at 1154. +- `check` builds executed check sets at 1270–1272, then calls `BLOCKERS` + at 1273, reaching `condition_matrix` (982) through 1000. All original + seven CONDITIONS and six CATEGORIES (room, queue, EV, staff, goods, + business state, represented by the key `business`) remain required. + Missing executed check names produce pending/deficits; no fixed-true matrix. +- Collector `tools/run_p23_life_observation.py`: `collect_os` (110) + invokes `collect_screenshots` (26) at 186. JPEG collection invokes + `convert_screenshot` (73) at 55; conversion calls `inspect_conversion` + at 107. Raw CUA bytes and tool-call bindings are retained separately from + converted PNG and conversion receipts. This wiring was inspected, not run. +- `diagnostic_stream.gd` captures hash-addressed `rest_audit` and `motion` + at 70–78. `rest_audit` (89) records selection inputs, route/cost probes, + locks/transit and comparison-only Runtime output. No stored-rest field was + introduced. The source-grounded correction supersedes the old stored-state + proposal below. MotionObserver retains two draw frames, not whole Runtime + state per frame. +- Native route: `tests/ui/test_p23_life_observation.gd` at 18–21 invokes + `layout_comparison.gd`; the runner invokes that UI test at 309 for A/B. + Layout comparison preloads diagnostics at 5 and starts watching at 65. +- Normal OS route is separate from the native UI component test: runner + RECORDER at 24 is copied/launched at 145/152; `passive_os_recorder.gd` + preloads `passive_observation.gd` at 2 and calls its inherited run at 6. + `passive_observation.gd` extends `observation.gd` at 1; + `observation.gd` preloads diagnostics/layout comparison at 3–4 and + watches the real preview at 26. These fixtures are reached through the + OS runner, not directly preloaded by the native UI test. +- Existing mutation negatives cover conversion provenance, rest operands, + motion history, rectangle/pixel transforms, process/source/profile guards, + formal members/distinct PID/ACK, and every condition/category matrix cell. + No concrete missing call or negative was found; no code hardening was added. + +## Exact local commands and real results + +- `git status --short`, `git diff --stat`, and targeted `git diff --` + for the collector, UI test, diagnostic fixture and marker: inspected existing + changes without modifying git state. +- Targeted `rg -n` / `rg -n -A...` searches for function definitions, + calls, fixture preloads/extends, rest_audit/motion, CONDITIONS/CATEGORIES + and unittest methods in the three P23 tools and five P23 fixtures/UI test: + wiring results recorded above. No Runtime/source audit or Phase0 repetition. +- `python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`: + initial attempt failed because the interpreter's default cache destination + under Library/Caches was not writable; no syntax failure was reported. +- `PYTHONPYCACHEPREFIX=docs/plans/original-completion-2026-10-02/execution/p23-03/.finalization-pycache python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`: + PASS, exit 0 for all three tools. +- `python3 -B tools/test_p23_life_observation_evidence.py`: + **27 test methods run, 27 passed**, 0 failures/errors; reported 0.044 seconds. + These are synthetic checker tests, not native/OS evidence. +- `git diff --check`: PASS, exit 0; repeated after this status rewrite. +- Read-only `python3 -B -c` JSON/hash/path assertions using + `git show 860136f7099f5e3aee99da5798a627a77f2d8eea:`, + `git diff --name-only ` and + `git ls-files --others --exclude-standard`: PASS. + All old marker metadata/rows equal base; exactly one appended P23-03 + required source-authority entry; test SHA + `cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1` + matches current bytes. suite-policy.json equals base byte-for-byte. + All 16 changed/untracked paths relative to base are allowed. +- The immutable spec SHA matches + `8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f` + using the existing checker's `read_text().strip().encode()` convention. + An initial extra raw-byte SHA assertion failed (raw file SHA + `1a925e0d7f5c1c9f28670d29dd0fdc5aa48f4d5415946d6de4c15e1d83b5ed66`); + normalized verification passed and the spec has no diff against base. +- An initial inline validation here-document could not create its shell + temporary file; rerun via `python3 -B -c` succeeded. +- Removed only the temporary .finalization-pycache tree created by this run. + No pre-existing dirty/untracked material was removed. + +## Root work still pending — expected, not worker blockers + +Native Godot execution; actual OS collector with screenshots for both layouts +and all six categories, FRONT/BACK and ordinary pan/zoom; formal save and +distinct actual PID normal resume; one full exact-candidate **177-suite** +regression; independent Opus all-condition review. These must precede any +acceptance determination. Constructibility, visual correctness and same-demand +improvement have not been claimed from static checks. + +## Superseded history — retained verbatim + +The following earlier blocked statuses (including the 19-method and +"not yet wired" claims) describe previous candidates and are superseded by the +current wiring and results above. + +# Current RUN continuation — blocked on stored-rest-destination contract + +All seven pre-existing modified files and three untracked fixtures were retained. +No git writes, agents, Godot, collector execution, Runner or full regression. +This is not a genuine completed submission and makes no acceptance claim. +Root native/OS evidence remains **pending**, independently of this source blocker. + +Changes completed in this continuation: +- Removed mandatory left-button drag and its pan-change assertion. Camera + screenshots now use ordinary-wheel-zoom, pan-mode-button, move-mode-wheel-pan. + Existing non-Ctrl event observation is preserved. +- Added stdlib PNG decoding with CRC/chunk/end checks, RGB/RGBA and filters 0–4. + JPEG dimensions reuse the P08 helper; no JPEG decoder or PIL was added. +- Added conversion receipt inspection and Root-only convert_screenshot helper: + separate output, original CUA hash/call binding, sips executable SHA/argv, + output digests, decoded PNG and magic-derived dimension equality. No actual + sips conversion was run here. This helper is not yet wired into collect_os. +- Added four image test methods including all filters, CRC corruption, trailing + data, converter SHA/argv, raw CUA hash, dimensions and missing-conversion negatives. + +Actually executed: 19 checker unittest methods passed; py_compile passed for +the three P23 Python tools. Temporary compilation outputs were removed. +Synthetic image tests are checker tests, never native screenshot evidence. + +Concrete contract gap (targeted dependency inspection, not a repeated Phase0 audit): +- autoload/simulation_runtime.gd:13109–13125 computes get_staff_rest_report by + scanning facilities and selecting a minimum-cost route on every call. The + selected facility and eligibility are local variables, not stored state. +- autoload/simulation_runtime.gd:8783–8785 calls that query and passes only its + cell to advance_staff_commute. At :12656–12690 the movement function either + returns without movement or emits a STAFF_MOVED event with a target_cell; + it does not persist a selected rest facility ID or eligibility. Movement + events cannot supply a current destination for stationary/nonmoving staff. +- scripts/staff/staff_runtime.gd:282–315 serializes current_cell, assignment, + task and path_handle but no rest destination/eligibility. Runtime snapshot + assembly at :511–570 has no stored rest-selection model. PathService's + snapshot at scripts/world/path_service.gd:1606 stores path/actor projections, + not the selected rest facility and its eligibility. + +Thus recipe (3), "rest destination must equal the typed stored destination and +its eligibility; do not re-simulate AI", has no authoritative stored operand. +Calling get_staff_rest_report from a fixture would rerun selection, and copying +its output into a purported stored-state field would manufacture authority. +These Runtime/staff/path files are outside allowed_paths and remain unchanged. + +Read source SHA256: +- Runtime: 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67 +- StaffRuntime: 807a417bc7866c97cd6d8f5c1368b5480a2d9fd685a2a47a978e81482bf522b5 +- PathService: 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4 + +Minimal separate proposal for Root/Opus, not applied: authorize an authoritative +rest-decision record at the existing Runtime selection point, including selected +facility/cell, eligibility and decision time, with explicit invalidation and +snapshot/restore semantics. Alternatively Root must identify an existing typed +stored field that has those semantics. No worker relaxation of this gate. + +Remaining source implementation is explicit: collector conversion integration, +all row/rectangle bindings, motion history, distinct-PID/formal ACK checker, +dynamic matrix and the remaining requested negatives. Existing fail-closed +whole-unit behavior remains; no unconditional PASS or new fail stub was added. +The 30/88 denominator, seven conditions, six categories and old suite policies +are unchanged. Do not begin acceptance assuming only Root evidence is missing. + +--- +Previous status retained as history: + +# Repair from source 03100be — blocked, implementation incomplete + +Source checkout HEAD confirmed read-only: `03100beabeb55caac03f53db3d1f399b424df517`. +Contract base remains `860136f7099f5e3aee99da5798a627a77f2d8eea`. +No git writes, Godot, Runner, OS input, regression, or other workers were used. +No acceptance or partial credit is claimed. No out-of-scope defect or missing Runtime capability was established. + +Changes in this repair: +- Added registered `--verify-only` CLI, concrete missing-index deficits, immutable fixture/spec/contract/baseline/AGENTS pins and a normative P23 plan-section hash. Full-plan SHA is separate provenance only. Existing diagnostic/layout CLI modes retained. +- Removed fixture-side begin_leaving/despawn_guest calls; Runtime owns continued guest behavior. Added A/B native entry to the existing component test and updated the existing single marker row SHA. +- Added observation.gd extending P08 placement helpers unchanged, with real supply scenario commands/settings, common definition-derived construction budget, seed806042, relocated common cafeteria/kitchen at x6 to avoid declared B elevator footprint, sparse typed captures, normal save/load and whole formal-model comparisons. These construction prechecks have not been executed. +- Added passive request/ACK endpoint and recorder for four layout/role combinations, live FRONT/BACK buttons and PanModeButton, ordinary non-Ctrl wheel zoom/pan and ordinary pan drag only. No deferred P23-02 held-gesture credit. +- Replaced the collector stub with an adaptation of P08 collect/collect_os using run_all_tests helpers and owned gates, fresh outputs/profiles, native import/parse/layout phases, four OS roles and formal reader copies. Records the current interpreter PIL availability; raw screenshot bytes are left untouched. Collector has not been executed. +- Checker now inspects available native/OS streams, comparisons and basic process/source/profile guards, but explicitly fails closed for outstanding whole-unit gates. + +Local verification actually executed: 16 Python unittest cases PASS; ast.parse for the three changed Python tools PASS; one P23 marker row and exact source SHA PASS. No engine or evidence-collector execution. + +Remaining IMPLEMENTATION, separate from missing Root evidence: +1. Complete screenshot byte-magic dimensions, stdlib PNG pixel/rect checks, and indexed Root JPEG-to-lossless-PNG conversion binding to raw hash/converter provenance. PIL absence is not waived. +2. Add prior-frame visual-position/frame-delta evidence and independent interpolation checks; compare rest destinations with typed stored destination/eligibility without re-simulating AI. +3. Bind all request report rows to viewport rectangles/affine and screenshots; current endpoint retains report plus viewport-to-screen transform but does not yet produce every required viewport row rectangle. +4. Finish independent distinct-PID/formal-member/resume-state/load-ACK gates. Fixture assertions do not replace checker proof. +5. Replace hard failure condition-matrix deficits with exact completed check names only after all seven independent gates exist; finish all requested category and infrastructure negatives. Current check intentionally cannot pass whole-unit integrity. +6. Statically review collector lifecycle/failure receipts and fixture category coverage; real constructibility and EV/passenger/queue/goods coverage remain unproved. + +Root native, actual OS for A/B, formal distinct-PID resume, exact-final full regression, independent review, integration/final/ledger: all PENDING. This candidate is blocked on remaining in-scope implementation, not ready for acceptance or a claim that missing Root evidence is the only gap. + +--- +Previous worker status retained verbatim as history (superseded by the repair status above): + +# P23-03 worker status: submitted static work, implementation incomplete + +Base commit: `860136f7099f5e3aee99da5798a627a77f2d8eea`. +This submission preserves the previous dirty work and adds genuine static +implementation. It is **not acceptance-ready**. No external Runtime capability +gap or out-of-scope defect has been established. Remaining implementation is +listed separately from pending Root evidence; no acceptance credit is claimed. + +## Inspection and changes in this repair + +- Re-read the recorded direction, Phase 0 map, previous status and renderer diff. + The single frame builder supplies drawing and the deep-copy public report. + No renderer redesign or additional renderer was introduced. Existing renderer + and component test bytes were retained. +- Added `tests/fixtures/p23-03/layout_comparison.gd`. Both layouts initialize the + public Runtime with seed 806042, reset budget and clock, identical production + defaults and the same recorded 48-person arrival script. Commands build the + existing elevator and corridors; B adds only the declared second elevator. + Preparation time derives from registered definition build times. +- Predeclared metric: sum of destination-arrival minutes for the entire cohort. + The checker rejects incomplete cohorts, changed actual arrival payloads, + additional layout changes, changed initial state, and non-strict improvement. + No observed improvement is claimed: the fixture has not run. +- Extended diagnostic_stream with hash-addressed typed event/input records. + The fixture records actual command results, domain movement events and + per-minute guest/path/EV/facility projections, including service history, + queues and density. Whole aggregate state/report pairs are sparse (30-minute + intervals); the existing P08 typed encoder is reused without truncation. +- Extended independent checking of room service/rescue occupants and business + enum fields, queue allocation/overflow, shared transforms, traffic, logistics + report selection/order and cabin geometry. Added Rect2 and vector-key decode. + The checker still refuses whole-unit acceptance. + +## Exact remaining implementation (not external blockers) + +1. Finish rest destination selection from typed path costs and event/defense + locks. Current room check validates eligibility and reported rest references; + it does not independently prove which rest destination Runtime selected. +2. Finish every actor/passenger mode and geometry, including cleaning, + rest/rescue/service slots, visual track history, badge text and screenshot + rect/pixel provenance. Cabin endpoints and rectangles are checked, but + unpaused prior-frame interpolation origin is not independently established. +3. Complete A/B definition/source/production pins and command/event/path + cross-validation, invoke the fixture from the Root collector, and add + representative real operational scenarios for all six categories and BACK. + The added experiment covers the fixed-demand FRONT transport comparison; + it does not establish actual service purchasing or all-six-category coverage. +4. Replace run_p23_life_observation.py readiness stub with the actual Root + collector using P08 owned-process/profile/source/engine/ordinary-audio and + CUA request/ACK helpers. It remains unchanged; it launches nothing. + Formal save, distinct-PID normal resume, both-layout FRONT/BACK/pan/zoom, + complete CUA receipts, all-run cleanup/protection and failure history remain + to be wired and independently checked. +5. Add missing meaningful negatives, especially normal OS/distinct-PID resume, + visual provenance and the remaining rest/motion branches. Existing 12 local + tests are not a complete seven-condition test matrix. + +Steps (2) and (3) of the repair direction are therefore still incomplete; +step (4) is not implemented. This is a partial static submission, not a claim +that the requested repair is finished. Do not begin acceptance on an assumption +that all worker deliverables are complete. + +## Checks actually run in this repair + +- `python3 -B tools/test_p23_life_observation_evidence.py`: 12 test methods, + zero failures. Synthetic Python examples validate rejection logic only. +- AST syntax: all three P23 Python tools parsed. +- Marker JSON: old rows and metadata unchanged; exactly one appended row; + its SHA matches the unchanged GDScript component test. +- `git diff --check`: passed. +- No Godot parsing/runtime, native, OS, Runner or full regression execution. +- No git writes, subagents, integration, new assets or production changes. + suite-policy.json was not changed; no new suite was registered. + +## Source SHA256 at local checks + +| File | SHA256 | +| --- | --- | +| scenes/main/world_preview.gd | add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0 | +| scripts/ui/room_presentation.gd | 55340b7b623532e861be1d21225635fd9169e2e40928597c1cd0458b99524dd9 | +| tests/ui/test_p23_life_observation.gd | b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0 | +| tests/fixtures/p23-03/diagnostic_stream.gd | ff178a094fc310b0640d923a15058c6f6fcf859551c14f6428a5709dcd24260e | +| tests/fixtures/p23-03/layout_comparison.gd | ddf00268b8e32e3c5c078ebb57ada321382ecc7c5fbd87efad7e9dfbe768df6b | +| tools/run_p23_life_observation.py | 53c2f0638d4a861cf4fcb664504c25df33ae5478207867654f1c8cdf58fa23c7 | +| tools/check_p23_life_observation_evidence.py | 318b253a08248a753b88902e382edc5f01cbce6f1ed8092359698b69d3bb0503 | +| tools/test_p23_life_observation_evidence.py | 0f20d3de20b44f46f8738c0d022fcc1179656014e85fe31754f00b87ec2c288a | + +## Root evidence: pending + +After genuine completion and freezing: both-layout native and actual normal OS +observation, full CUA trace, formal save and distinct-PID resume, mandatory exact +version full regression and independent Opus review. These absent Root results +are **pending**, not blocked. The fixed 30-package / 88-unit denominator and all +P08/P22/P25/Windows/held-gesture/performance exclusions remain unchanged. diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.json new file mode 100644 index 0000000..07130de --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.json @@ -0,0 +1,922 @@ +{ + "errors": [ + "offline rectangle coverage" + ], + "expected_row_count": 10, + "images": [ + { + "conversion_sha256": "d9e92a5b1925d6e1855f6eaff2820007d7db8dd79a5348ec801a38251b40fe90", + "raw_sha256": "ab073ed1055ca2356a050fdd47176c454c109ebe5c008b194ea8a5f149efa7f5", + "receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860", + "request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167", + "whole_image_sha256": "206a9aafdc50d1f2f330690e14390de8c7ed5b0df024cedf8f2fbd35326f455a" + }, + { + "conversion_sha256": "b25b2224963f9a02ad87675969ab51c90aff29891b136095198afc2fe95acb73", + "raw_sha256": "7027a7f1813aa8c9fcff38e6e4b022ed718d6f9c9fd13d31ecc3d6d10818ac10", + "receipt_sha256": "184d50e702232ce259ca75b830992cb8e91d5175289a21812661325bf0024860", + "request_sha256": "172dc9a8f4bb684583f7417ac4c544172ee66b4134845fe540326df5598e6167", + "whole_image_sha256": "e6ab48ad864a1b5fdca2da587d95fe7949ac0c59e664a4524db66e3e861a73aa" + } + ], + "mechanical_pass": false, + "rows": [ + { + "category": "room", + "crops": [ + { + "image_index": 0, + "path": "row-0000-image-0.png", + "sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286" + }, + { + "image_index": 1, + "path": "row-0000-image-1.png", + "sha256": "0811d7a2ac82283233b2b5179ddc6ac32a86fac3a211febae2c706ba36513286" + } + ], + "dimmed": true, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": null, + "id": "facility_00000002", + "layer": 0, + "local_rect": [ + 131.0, + 388.0, + 256.0, + 192.0 + ], + "mechanical_pass": false, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 0, + "screenshot_rect": [ + 210.0, + 485.0, + 256.0, + 192.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "text": null + }, + { + "category": "room", + "crops": [ + { + "image_index": 0, + "path": "row-0001-image-0.png", + "sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69" + }, + { + "image_index": 1, + "path": "row-0001-image-1.png", + "sha256": "bb79fa3c9cc46f2f7b9d31b2de85c3b6f6b4446718d3614cfeeba818ea87cb69" + } + ], + "dimmed": true, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": null, + "id": "facility_00000003", + "layer": 0, + "local_rect": [ + 1027.0, + 388.0, + 256.0, + 192.0 + ], + "mechanical_pass": false, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 1, + "screenshot_rect": [ + 1106.0, + 485.0, + 256.0, + 192.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "text": null + }, + { + "category": "room", + "crops": [ + { + "image_index": 0, + "path": "row-0002-image-0.png", + "sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b" + }, + { + "image_index": 1, + "path": "row-0002-image-1.png", + "sha256": "b90d74efdd21be0eb69555138dccacb62100825a908c6f17958e17a46321056b" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": null, + "id": "facility_00000007", + "layer": 0, + "local_rect": [ + 131.0, + 292.0, + 128.0, + 96.0 + ], + "mechanical_pass": false, + "operation": "OPERATING", + "reason": "whole rectangle in viewport", + "row_index": 2, + "screenshot_rect": [ + 210.0, + 389.0, + 128.0, + 96.0 + ], + "state": "OPEN", + "state_name": "OPEN", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "text": null + }, + { + "category": "queue", + "crops": [ + { + "image_index": 0, + "path": "row-0003-image-0.png", + "sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac" + }, + { + "image_index": 1, + "path": "row-0003-image-1.png", + "sha256": "eeb6b5a301d017693ba3acd47d22b63348bc6536c5ddcfd593aa0d593e256aac" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": 36.0, + "id": "0", + "layer": 0, + "local_rect": [ + 259.0, + 276.0, + 42.0, + 14.0 + ], + "mechanical_pass": false, + "operation": null, + "reason": "whole rectangle in viewport", + "row_index": 3, + "screenshot_rect": [ + 338.0, + 373.0, + 42.0, + 14.0 + ], + "state": { + "members": [ + "guest/guest-auto-00000019" + ], + "overflow": 0, + "visible_count": 1 + }, + "state_name": null, + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "待ち1人" + }, + { + "category": "guest", + "crops": [ + { + "image_index": 0, + "path": "row-0004-image-0.png", + "sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34" + }, + { + "image_index": 1, + "path": "row-0004-image-1.png", + "sha256": "e08ebb7ae2eb1f2f66360df04d7c132bed923ece9ba5d406c72f23c16bc71e34" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": null, + "id": "guest/guest-auto-00000019", + "layer": 0, + "local_rect": [ + 267.0, + 325.0, + 48.0, + 54.0 + ], + "mechanical_pass": false, + "operation": null, + "reason": "whole rectangle in viewport", + "row_index": 4, + "screenshot_rect": [ + 346.0, + 422.0, + 48.0, + 54.0 + ], + "state": "QUEUING", + "state_name": "QUEUING", + "style_colors": [ + "688da0", + "b87659", + "a69a63", + "89789e", + "638d72", + "edc49d", + "c99371", + "a56f50", + "303c40", + "719d94", + "eee5c9", + "608d84", + "eee6c7", + "e3dfc2", + "956940", + "d3ad6b", + "b48850", + "af9059", + "d8bc78", + "f3ebcf", + "efe7ce", + "805e44" + ], + "style_source": "scripts/ui/pixel_people.gd", + "style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c", + "text": null + }, + { + "category": "guest", + "crops": [ + { + "image_index": 0, + "path": "row-0005-image-0.png", + "sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522" + }, + { + "image_index": 1, + "path": "row-0005-image-1.png", + "sha256": "8b4be9869042638da9a5afdd90ce982baa16623c2b5e4cbedc50109680867522" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": null, + "id": "guest/guest-auto-00000020", + "layer": 0, + "local_rect": [ + 139.0, + 301.0, + 48.0, + 54.0 + ], + "mechanical_pass": false, + "operation": null, + "reason": "whole rectangle in viewport", + "row_index": 5, + "screenshot_rect": [ + 218.0, + 398.0, + 48.0, + 54.0 + ], + "state": "RECEIVING_SERVICE", + "state_name": "RECEIVING_SERVICE", + "style_colors": [ + "688da0", + "b87659", + "a69a63", + "89789e", + "638d72", + "edc49d", + "c99371", + "a56f50", + "303c40", + "719d94", + "eee5c9", + "608d84", + "eee6c7", + "e3dfc2", + "956940", + "d3ad6b", + "b48850", + "af9059", + "d8bc78", + "f3ebcf", + "efe7ce", + "805e44" + ], + "style_source": "scripts/ui/pixel_people.gd", + "style_source_sha256": "a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c", + "text": null + }, + { + "category": "business", + "crops": [ + { + "image_index": 0, + "path": "row-0006-image-0.png", + "sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a" + }, + { + "image_index": 1, + "path": "row-0006-image-1.png", + "sha256": "62525c0b9f2c189545666c134eb6f83b660d6efbc0698278ce049ace7e708d0a" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": 73.0, + "id": "0", + "layer": 0, + "local_rect": [ + 133.0, + 390.0, + 79.0, + 14.0 + ], + "mechanical_pass": false, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 6, + "screenshot_rect": [ + 212.0, + 487.0, + 79.0, + 14.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "職員不足 未稼働" + }, + { + "category": "business", + "crops": [ + { + "image_index": 0, + "path": "row-0007-image-0.png", + "sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d" + }, + { + "image_index": 1, + "path": "row-0007-image-1.png", + "sha256": "379c49ce02077552289ae503c74a4a0032150ff6cec3ea20c42045f6a059ec3d" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": 73.0, + "id": "1", + "layer": 0, + "local_rect": [ + 1029.0, + 390.0, + 79.0, + 14.0 + ], + "mechanical_pass": false, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 7, + "screenshot_rect": [ + 1108.0, + 487.0, + 79.0, + 14.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "職員不足 未稼働" + }, + { + "category": "business", + "crops": [ + { + "image_index": 0, + "path": "row-0008-image-0.png", + "sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5" + }, + { + "image_index": 1, + "path": "row-0008-image-1.png", + "sha256": "79ea1c2d0ff2d4e8f6c7ff20f019a4e264c480438e7528b8de85709b8d8eeda5" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [ + "offline rectangle coverage" + ], + "font_width": 69.0, + "id": "2", + "layer": 0, + "local_rect": [ + 133.0, + 294.0, + 75.0, + 14.0 + ], + "mechanical_pass": false, + "operation": "OPERATING", + "reason": "whole rectangle in viewport", + "row_index": 8, + "screenshot_rect": [ + 212.0, + 391.0, + 75.0, + 14.0 + ], + "state": "OPEN", + "state_name": "OPEN", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "営業中 利用1人" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.sha256 b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.sha256 new file mode 100644 index 0000000..c70586c --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.sha256 @@ -0,0 +1 @@ +646334f4ef3c78a8cfe5d68d3738c4d772cdd71cbf80b2dba7c153693ead4419 diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-0.png new file mode 100644 index 0000000..83c5dfa Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-1.png new file mode 100644 index 0000000..83c5dfa Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-0.png new file mode 100644 index 0000000..3fc0cf1 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-1.png new file mode 100644 index 0000000..3fc0cf1 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-0.png new file mode 100644 index 0000000..53636cf Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-1.png new file mode 100644 index 0000000..53636cf Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-0.png new file mode 100644 index 0000000..8a339c9 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-1.png new file mode 100644 index 0000000..8a339c9 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-0.png new file mode 100644 index 0000000..85ee621 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-1.png new file mode 100644 index 0000000..85ee621 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-0.png new file mode 100644 index 0000000..85e9324 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-1.png new file mode 100644 index 0000000..85e9324 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-0.png new file mode 100644 index 0000000..d28700d Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-1.png new file mode 100644 index 0000000..d28700d Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-0.png new file mode 100644 index 0000000..282dc00 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-1.png new file mode 100644 index 0000000..282dc00 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-0.png new file mode 100644 index 0000000..88da401 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-1.png new file mode 100644 index 0000000..88da401 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.json new file mode 100644 index 0000000..d3a163b --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.json @@ -0,0 +1,443 @@ +{ + "errors": [], + "expected_row_count": 4, + "images": [ + { + "conversion_sha256": "363c2bdb1da77458226d01e971b9aea1220fcc81f70fb5c3c816a12b7039fac3", + "raw_sha256": "2abb4675f9978e7b86599c4334732ebadcb7cd5c5c51deeacaa7d0dee858e9b9", + "receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17", + "request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093", + "whole_image_sha256": "abf7d2e89413431438f35cc3eecb5534a7257cb033906c5397592678b2e9bce4" + }, + { + "conversion_sha256": "d9f5e79103258a50bb5d0866725a8488017a9cc8ef150312d16de9043618b55c", + "raw_sha256": "e5a7ac0c3ba3f001baae7c771304d1a9a9cf024240176e8f8735a9dcf1091b31", + "receipt_sha256": "b393e4e0afcdbc72f8cbb3c88cbe6cab7fa2548855ee648c69c5ac61f7fffb17", + "request_sha256": "1a9ace68b3cf0ca997ea3e28ba1da1c30aa0cff72bf5d433d008ce578c7de093", + "whole_image_sha256": "b82e6ef061d9b4e8cbc3321405553f3f447c8abfb61ce38532254130a704ee95" + } + ], + "mechanical_pass": true, + "rows": [ + { + "category": "room", + "crops": [ + { + "image_index": 0, + "path": "row-0000-image-0.png", + "sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43" + }, + { + "image_index": 1, + "path": "row-0000-image-1.png", + "sha256": "94beea581b9586cda35d6c16a0bb8dccc7cad6c6cd0a9842ead3b34d19066f43" + } + ], + "dimmed": true, + "eligibility": "visible", + "failures": [], + "font_width": null, + "id": "facility_00000004", + "layer": 1, + "local_rect": [ + 131.0, + 388.0, + 256.0, + 192.0 + ], + "mechanical_pass": true, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 0, + "screenshot_rect": [ + 210.0, + 485.0, + 256.0, + 192.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "text": null + }, + { + "category": "room", + "crops": [ + { + "image_index": 0, + "path": "row-0001-image-0.png", + "sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54" + }, + { + "image_index": 1, + "path": "row-0001-image-1.png", + "sha256": "713f0f370de8ff40def7b5fbe9f071b8f5be3db1604b18bce5c6cf7e8eb62c54" + } + ], + "dimmed": true, + "eligibility": "visible", + "failures": [], + "font_width": null, + "id": "facility_00000005", + "layer": 1, + "local_rect": [ + 1027.0, + 388.0, + 256.0, + 192.0 + ], + "mechanical_pass": true, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 1, + "screenshot_rect": [ + 1106.0, + 485.0, + 256.0, + 192.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "34413b", + "8e6348", + "c39665", + "eee5c9", + "608d84", + "c6b999", + "a6b6a0", + "d4c7a5", + "acb9a5", + "897487", + "f1e9cb", + "b39b77", + "615563", + "a57852", + "587f7b", + "edc97c", + "d6ad52", + "709b94", + "a37b91", + "72b6b8", + "c1e0d4", + "445652", + "a2b2a3", + "d3aa8f", + "ca9c62", + "ffe9a8", + "6c8993", + "adc5b4", + "52836c", + "c7c6a6", + "a8b6aa", + "d8bf89", + "aec0bf", + "8a7155", + "617f82", + "3e514e", + "404f63", + "83958a", + "283a39", + "586c62", + "526177", + "dcc08b", + "9daa8d", + "879eaa", + "556e62", + "4d6472", + "e2bd6c", + "ba7157", + "bfbb9a", + "a9aa91", + "9ab9b3", + "d5e3d0", + "f2ca81", + "fff0d6", + "a67c53" + ], + "style_source": "scripts/ui/pixel_tower_art.gd", + "style_source_sha256": "71df9b8400cb883a5bb1c89729f80c77e734df503fc54154be3e486adb8391df", + "text": null + }, + { + "category": "business", + "crops": [ + { + "image_index": 0, + "path": "row-0002-image-0.png", + "sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae" + }, + { + "image_index": 1, + "path": "row-0002-image-1.png", + "sha256": "a5ba5edbf9706e2b8a3ceb1de3d61e11074fbb731316e9686e2cec5ea98fc2ae" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [], + "font_width": 73.0, + "id": "0", + "layer": 1, + "local_rect": [ + 133.0, + 390.0, + 79.0, + 14.0 + ], + "mechanical_pass": true, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 2, + "screenshot_rect": [ + 212.0, + 487.0, + 79.0, + 14.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "職員不足 未稼働" + }, + { + "category": "business", + "crops": [ + { + "image_index": 0, + "path": "row-0003-image-0.png", + "sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869" + }, + { + "image_index": 1, + "path": "row-0003-image-1.png", + "sha256": "e977d7db178d1d61310fe7e33a9c25873b62b584337d93ea29960d6f8c5bb869" + } + ], + "dimmed": false, + "eligibility": "visible", + "failures": [], + "font_width": 73.0, + "id": "1", + "layer": 1, + "local_rect": [ + 1029.0, + 390.0, + 79.0, + 14.0 + ], + "mechanical_pass": true, + "operation": "INACTIVE", + "reason": "whole rectangle in viewport", + "row_index": 3, + "screenshot_rect": [ + 1108.0, + 487.0, + 79.0, + 14.0 + ], + "state": "NO_STAFF", + "state_name": "NO_STAFF", + "style_colors": [ + "b5cbb5", + "273c49", + "526d68", + "233d40", + "8cae98", + "749981", + "567e68", + "c6d3b6", + "d5dec4", + "574d39", + "776445", + "8a7350", + "69583d", + "65583e", + "385d45", + "47463e", + "5a594b", + "aaa083", + "829a8d", + "b69465", + "514839", + "8d7951", + "e1bd6c", + "dfd4ad", + "2f493f", + "e4d9b9", + "58784c", + "4c7950", + "5e8b57", + "739957", + "95ad69", + "648957", + "e34332", + "e3a62f", + "55c96b", + "f5d582", + "34413b", + "fff2ce", + "3c4e40", + "f5d58a", + "304b41", + "f0e2b9", + "303c40", + "d69a56", + "4ca5c7", + "efbd72", + "79d3e7", + "e5f8fa", + "64d48c", + "e16262", + "8ff0ab", + "ff8a80", + "52bdd0", + "8ce6ee", + "d7ebef", + "29483d", + "f0e2bd", + "8ca4af", + "6f8b67", + "427582", + "a24b45", + "a77d3f", + "6b6258", + "89914d", + "6f9a55", + "67518e", + "ffd080" + ], + "style_source": "scenes/main/world_preview.gd", + "style_source_sha256": "add770377c190a5ccdde95379539de9753f7dabc703c1d58452ba590c6eda2f0", + "text": "職員不足 未稼働" + } + ] +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.sha256 b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.sha256 new file mode 100644 index 0000000..43d528a --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.sha256 @@ -0,0 +1 @@ +f013e0d1d6231833b656859cc73ad0b58b389047e421ad2c43cc598820e21067 diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-0.png new file mode 100644 index 0000000..fd1e793 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-1.png new file mode 100644 index 0000000..fd1e793 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-0.png new file mode 100644 index 0000000..c61ce2b Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-1.png new file mode 100644 index 0000000..c61ce2b Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-0.png new file mode 100644 index 0000000..ee65f5a Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-1.png new file mode 100644 index 0000000..ee65f5a Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-1.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-0.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-0.png new file mode 100644 index 0000000..19c41e1 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-0.png differ diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-1.png b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-1.png new file mode 100644 index 0000000..19c41e1 Binary files /dev/null and b/docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-1.png differ diff --git a/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index b6a87a2..fa32fe0 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -78,6 +78,9 @@ var _queue_positions: Dictionary = {} var _queue_hidden: Dictionary = {} var _landing_badges: Array[Dictionary] = [] var _transport_report: Dictionary = {} +var _life_observation: Dictionary = {} +var _life_overlay: Node2D +var _life_observation_serial := 0 var _dragging := false var _drag_button := MOUSE_BUTTON_NONE @@ -103,6 +106,10 @@ func _ready() -> void: _inactive_layer_art = InactiveLayerRooms.new() _inactive_layer_art.name = "InactiveLayerRooms" add_child(_inactive_layer_art) + _life_overlay = Node2D.new() + _life_overlay.name = "LifeOverlay" + _life_overlay.draw.connect(_draw_life_overlay) + add_child(_life_overlay) _layer_lens = ColorRect.new() _layer_lens.name = "LayerLens" _layer_lens.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -511,6 +518,9 @@ func _draw() -> void: func _draw_contents() -> void: + _life_observation = {} + if _life_overlay != null: + _life_overlay.queue_redraw() if _inactive_layer_art != null: _inactive_layer_art.visible = false _draw_background() @@ -521,18 +531,27 @@ func _draw_contents() -> void: var map_rect := get_map_rect() if map_rect.size == Vector2.ZERO: return + _life_observation = _build_life_observation() + _life_observation_serial += 1 + _life_observation[&"observation_serial"] = _life_observation_serial + _life_observation[&"overlay_consumed_serial"] = -1 + _room_presentations = _life_observation[&"rooms"] + _carried_patients = _life_observation[&"carried_patients"] + _treated_patients = _life_observation[&"treated_patients"] + _resting_staff = _life_observation[&"resting_staff"] + _queue_positions = _life_observation[&"queue_positions"] + _queue_hidden = _life_observation[&"queue_hidden"] + _landing_badges.assign(_life_observation[&"landing_badges"]) + _transport_report = _life_observation[&"elevators"] + _boarded_actor_ids = _life_observation[&"boarded"] + _actor_tracks = _life_observation[&"actor_tracks"] + _elevator_tracks = _life_observation[&"elevator_tracks"] _draw_world_cells(bounds, map_rect) - if show_grid: - _draw_congestion_overlay(bounds) - _prepare_room_presentations() - _prepare_queues() _draw_world_entity_icons() _draw_legacy_stairs() - _draw_elevators() _draw_other_layer() _draw_construction_labels() _draw_facility_labels() - _draw_actors() _draw_excavation_jobs() _draw_build_preview() _draw_excavation_preview() @@ -540,6 +559,177 @@ func _draw_contents() -> void: _draw_map_caption(map_rect) +## The report describes the last submitted draw, not an independently sampled world. +## Callers cannot mutate either the frame or its interpolation tracks. +func get_life_observation_report() -> Dictionary: + return _life_observation.duplicate(true) + + +## Pure with respect to Runtime and this Control: helpers mutate only fresh local +## dictionaries (including copies of visual tracks). No whole Runtime snapshot +## is copied per frame; whole typed snapshots belong to sparse diagnostics. +func _build_life_observation() -> Dictionary: + var model := _observe_rooms() + model.merge(_observe_queues()) + model[&"layer"] = layer_mode + model[&"zoom"] = zoom_level + model[&"pan"] = pan_offset + model[&"viewport"] = size + model[&"animation_time"] = _animation_time + model[&"actor_tracks"] = _actor_tracks.duplicate(true) + model[&"elevator_tracks"] = _elevator_tracks.duplicate(true) + model[&"boarded"] = {} + var clock := get_node_or_null("/root/GameClock") + model[&"paused"] = clock != null and bool(clock.get("paused")) + model[&"cabins"] = _observe_elevators(model) + model[&"actor_drawings"] = _observe_actors(model) + var guests: Array[Dictionary] = [] + var staff: Array[Dictionary] = [] + var tasks: Array[Dictionary] = [] + var guest_service: Variant = _service(&"guest_service") + if guest_service != null and guest_service.has_method(&"get_guests"): + for guest: GuestRuntime in guest_service.get_guests(): + guests.append({&"id": guest.guest_id, &"state": GuestRuntime.State.keys()[guest.state], + &"cell": guest.current_cell, &"target": guest.target_facility_id, + &"service": guest.current_service_id, &"queue": guest.queue_request_id}) + var staff_service: Variant = _service(&"staff_service") + if staff_service != null: + for member: StaffRuntime in staff_service.get_staff_members(): + staff.append({&"id": member.staff_id, &"state": StaffRuntime.State.keys()[member.state], + &"cell": member.current_cell, &"task": member.current_task_id, + &"facility": member.assigned_facility_id, &"role": member.role_id, + &"carried_inventory_id": member.carried_inventory_id}) + for task: StaffTask in staff_service.get_tasks(): + tasks.append({&"id": task.task_id, &"state": StaffTask.State.keys()[task.state], + &"type": task.task_type, &"staff": task.assigned_staff_id, + &"target": task.target_entity_id, &"rescue_facility": task.rescue_facility_id}) + model[&"guests"] = guests + model[&"staff"] = staff + model[&"tasks"] = tasks + _fit_life_actors(model) + var bridge: Variant = _service(&"supply_logistics") + var goods: Dictionary = bridge.get_report() if bridge != null else {&"requests": [], &"deliveries": []} + model[&"goods"] = goods + var badges: Array[Dictionary] = [] + var facilities: Variant = _service(&"facility_service") + var world: Variant = _service(&"world_service") + if facilities != null and world != null: + for instance: FacilityInstance in facilities.get_instances(): + if instance.origin.z != layer_mode: + continue + var cells: Array[Vector3i] = world.get_entity_cells(instance.instance_id) + if cells.is_empty(): + continue + var rect := _cell_bounds_rect(cells) + var data: Dictionary = model[&"rooms"][instance.instance_id] + data[&"rect"] = rect + data[&"definition_id"] = instance.definition_id + var count := RoomPresentation.slots(rect, _cell_dimensions().x / 16.0, instance.definition_id).size() + badges.append({&"facility": instance.instance_id, &"kind": &"business", + &"at": rect.position + Vector2(2, 2), + &"text": RoomPresentation.business_label(data) + " " + RoomPresentation.label(data, count)}) + var requests: Array[Dictionary] = [] + for request: Dictionary in goods.get(&"requests", []): + if request[&"source_facility_id"] == instance.instance_id or request[&"destination_facility_id"] == instance.instance_id: + requests.append(request) + if not requests.is_empty(): + badges.append({&"facility": instance.instance_id, &"kind": &"supply", + &"at": rect.position + Vector2(2, 16), &"requests": requests, + &"text": RoomPresentation.supply_label(requests)}) + model[&"badges"] = badges + _resolve_observation_labels(model) + var obstacles: Array[Rect2] = [] + for cabin: Dictionary in model[&"cabins"]: + obstacles.append(cabin[&"rect"]) + obstacles.append(cabin[&"badge_rect"]) + for collection: Array in [badges, model[&"landing_badges"], model.get(&"actor_aggregations", [])]: + for badge: Dictionary in collection: + obstacles.append(_readable_badge_rect(badge[&"at"], badge[&"text"])) + model[&"traffic_obstacles"] = obstacles + var traffic: Array[Dictionary] = [] + var path: Variant = _service(&"path_service") + var bounds := _world_bounds() + if path != null and not bounds.is_empty(): + for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1): + for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1): + var cell := Vector3i(x, y, layer_mode) + var row: Dictionary = path.get_cell_traffic(cell) + if not row.is_empty(): + var cell_rect := get_cell_rect(cell) + var rect := cell_rect.grow(-2.0) + var world_pixel := _cell_dimensions().x / 16.0 + var thickness := clampf(rect.size.y * 0.16, 2.0, 5.0) + var band := Rect2(Vector2(rect.position.x + rect.size.x * 0.12, + cell_rect.end.y - 2 * world_pixel - thickness * 0.5), + Vector2(rect.size.x * 0.76, thickness)) + traffic.append({&"cell": cell, &"traffic": row, &"rect": rect, &"band_rect": band, + &"segment_rects": _traffic_segments(band, rect, obstacles)}) + model[&"traffic"] = traffic + return model + + +## Subtract opaque observation regions, retaining a visible density mark even +## when the ordinary floor band is completely behind a moving cabin. +func _traffic_segments(band: Rect2, cell: Rect2, obstacles: Array[Rect2]) -> Array[Rect2]: + for y: float in [band.position.y, cell.position.y, cell.end.y - band.size.y]: + var pieces: Array[Rect2] = [Rect2(Vector2(band.position.x, y), band.size)] + for obstacle: Rect2 in obstacles: + var remaining: Array[Rect2] = [] + for piece: Rect2 in pieces: + var cut := piece.intersection(obstacle) + if not cut.has_area(): + remaining.append(piece) + continue + for part: Rect2 in [Rect2(piece.position, Vector2(piece.size.x, cut.position.y-piece.position.y)), + Rect2(Vector2(piece.position.x, cut.end.y), Vector2(piece.size.x, piece.end.y-cut.end.y)), + Rect2(Vector2(piece.position.x, cut.position.y), Vector2(cut.position.x-piece.position.x, cut.size.y)), + Rect2(Vector2(cut.end.x, cut.position.y), Vector2(piece.end.x-cut.end.x, cut.size.y))]: + if part.has_area(): + remaining.append(part) + pieces = remaining + if not pieces.is_empty(): + return pieces + return [] + + +func _resolve_observation_labels(model: Dictionary) -> void: + var placed: Array[Dictionary] = [] + for i: int in model[&"cabins"].size(): + var cabin: Dictionary = model[&"cabins"][i] + placed.append({&"id": "cabin/%d/rect" % i, &"rect": cabin[&"rect"]}) + placed.append({&"id": "cabin/%d/badge" % i, &"rect": cabin[&"badge_rect"]}) + for kind: String in ["business", "supply", "landing", "aggregation"]: + var collection: Array = model[&"badges"] if kind in ["business", "supply"] else model[&"landing_badges"] if kind == "landing" else model.get(&"actor_aggregations", []) + for i: int in collection.size(): + var badge: Dictionary = collection[i] + if kind in ["business", "supply"] and badge[&"kind"] != kind: + continue + var anchor: Vector2 = badge[&"at"] + var at := anchor + var displaced_by: Array[String] = [] + # Each collision advances to an existing right edge. Positive widths + # guarantee termination; touching edges are allowed. + while true: + var blocker: Dictionary = {} + for previous: Dictionary in placed: + if _readable_badge_rect(at, badge[&"text"]).intersection(previous[&"rect"]).has_area(): + blocker = previous + break + if blocker.is_empty(): + break + displaced_by.append(blocker[&"id"]) + at.x = (blocker[&"rect"] as Rect2).end.x + badge[&"anchor"] = anchor + badge[&"at"] = at + badge[&"displaced_by"] = displaced_by.duplicate(true) + placed.append({&"id": "%s/%d" % [kind, i], &"rect": _readable_badge_rect(at, badge[&"text"])}) + + +func _draw_life_badges(canvas: CanvasItem) -> void: + for badge: Dictionary in _life_observation.get(&"badges", []): + _draw_readable_badge(badge[&"at"], badge[&"text"], canvas) + + func _draw_background() -> void: var night := false var clock := get_node_or_null("/root/GameClock") @@ -650,40 +840,43 @@ func _draw_leaf_cluster(center: Vector2, p: float, variation: int) -> void: draw_rect(Rect2(c+offset,Vector2(3+i%2,2)*p),Color("95ad69") if i%3==0 else Color("648957")) -func _draw_congestion_overlay(bounds: Dictionary) -> void: - var path_service: Variant = _service(&"path_service") - if path_service == null or not path_service.has_method(&"get_cell_traffic"): +func _draw_life_overlay() -> void: + if _life_observation.is_empty(): return - for y in range(int(bounds[&"min_y"]), int(bounds[&"max_y"]) + 1): - for x in range(int(bounds[&"min_x"]), int(bounds[&"max_x"]) + 1): - var cell := Vector3i(x, y, layer_mode) - var traffic_value: Variant = path_service.call(&"get_cell_traffic", cell) - if not traffic_value is Dictionary or traffic_value.is_empty(): - continue - var traffic: Dictionary = traffic_value - var density := int(traffic.get(&"current_density", 0)) - var capacity := maxi(int(traffic.get(&"base_capacity", 1)), 1) - var blocked := bool(traffic.get(&"temporary_block", false)) - if density <= 0 and not blocked: - continue - var ratio := float(density) / float(capacity) - var color := Color("55c96b") - if blocked or ratio > 1.0: - color = Color("e34332") - elif ratio >= 0.65: - color = Color("e3a62f") - var rect := get_cell_rect(cell).grow(-2.0) - var center := rect.get_center() - var route_width := clampf(rect.size.y * 0.16, 2.0, 5.0) - draw_line( - Vector2(rect.position.x + rect.size.x * 0.12, center.y), - Vector2(rect.end.x - rect.size.x * 0.12, center.y), - Color(color, 0.90), - route_width - ) - if blocked: - draw_line(rect.position, rect.end, color, 2.0) - draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), color, 2.0) + # These are actual child indices and the passes executed by this callback. + _life_observation[&"overlay_child_indices"] = [_inactive_layer_art.get_index(), _life_overlay.get_index(), _layer_lens.get_index()] + _life_observation[&"overlay_passes"] = [] + _life_observation[&"overlay_passes"].append(&"traffic") + _draw_congestion_overlay(_life_overlay) + _life_observation[&"overlay_passes"].append(&"cabins") + _draw_elevators(_life_overlay) + _draw_life_badges(_life_overlay) + _life_observation[&"overlay_passes"].append(&"actors") + for cabin: Dictionary in _life_observation.get(&"cabins", []): + for actor: Dictionary in cabin[&"passengers"]: + _draw_observed_actor(actor, _life_overlay) + _draw_actors(_life_overlay) + for badge: Dictionary in _life_observation.get(&"actor_aggregations", []): + _draw_readable_badge(badge[&"at"], badge[&"text"], _life_overlay) + _life_observation[&"overlay_consumed_serial"] = _life_observation[&"observation_serial"] + + +func _draw_congestion_overlay(canvas: CanvasItem) -> void: + for row: Dictionary in _life_observation.get(&"traffic", []): + var traffic: Dictionary = row[&"traffic"] + var density := int(traffic[&"current_density"]) + var blocked := bool(traffic[&"temporary_block"]) + if density <= 0 and not blocked: + continue + var ratio := float(density) / maxi(int(traffic[&"base_capacity"]), 1) + var color := Color("e34332") if blocked or ratio > 1.0 else Color("e3a62f") if ratio >= 0.65 else Color("55c96b") + # At overview zoom a whole cell can be covered by opaque badges. Still + # submit its density band, beneath those later draws, and report no visible + # fragments. No traffic cell or raw density row is dropped. + if row[&"segment_rects"].is_empty(): + canvas.draw_rect(row[&"band_rect"], Color(color, 0.90)) + for segment: Rect2 in row[&"segment_rects"]: + canvas.draw_rect(segment, Color(color, 0.90)) ## Visual-only level comes from the existing live FacilityInstance, not the @@ -794,29 +987,40 @@ func _draw_world_entity_icons() -> void: draw_rect(Rect2(rect.end.x-4*p,rect.position.y+2*p,2*p,2*p),_facility_color(instance_id)) if instance_id==selected_entity_id: draw_rect(rect.grow(-p),Color("f5d582"),false,2*p) - var data: Dictionary = _room_presentations.get(instance_id, {}) - if not data.is_empty() and (rect.size.x >= 80 or instance_id == selected_entity_id) and id not in [&"guest_corridor", &"service_corridor", &"guest_stairs", &"service_stairs", &"guest_elevator", &"service_elevator"]: - var status := RoomPresentation.label(data, RoomPresentation.slots(rect, p, id).size()) - _draw_readable_badge(rect.position+Vector2(2, 2), status.replace("利用", "治療") if id == &"infirmary" else status.replace("利用", "休憩") if id in [&"staff_waiting_room", &"staff_dormitory"] else status) -func _draw_readable_badge(at: Vector2, text: String) -> void: - var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6 - var rect := Rect2(at, Vector2(width, 14)) +func _readable_badge_rect(at: Vector2, text: String) -> Rect2: + # A conservative em-width box makes geometry independently reproducible. + # Text still uses the existing fallback font and size. + return Rect2(at, Vector2(text.length() * 10 + 6, 14)) + + +func _draw_readable_badge(at: Vector2, text: String, canvas: CanvasItem = null) -> void: + if canvas == null: + canvas = self + var rect := _readable_badge_rect(at, text) if not rect.intersects(Rect2(Vector2.ZERO, size)): return - draw_rect(rect, Color("34413b", 0.94)) - draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce")) + canvas.draw_rect(rect, Color("34413b")) + canvas.draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce")) func _prepare_room_presentations() -> void: - _room_presentations.clear() - _carried_patients.clear() - _treated_patients.clear() - _resting_staff.clear() + var model := _observe_rooms() + _room_presentations = model[&"rooms"] + _carried_patients = model[&"carried_patients"] + _treated_patients = model[&"treated_patients"] + _resting_staff = model[&"resting_staff"] + + +func _observe_rooms() -> Dictionary: + var _room_presentations: Dictionary = {} + var _carried_patients: Dictionary = {} + var _treated_patients: Dictionary = {} + var _resting_staff: Dictionary = {} var service: Variant = _service(&"facility_service") if service == null: - return + return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff} for instance: Variant in service.get_instances(): if instance.origin.z == layer_mode: _room_presentations[instance.instance_id] = RoomPresentation.describe(instance) @@ -844,12 +1048,22 @@ func _prepare_room_presentations() -> void: else: _carried_patients[task.target_entity_id] = task.assigned_staff_id + return {&"rooms": _room_presentations, &"carried_patients": _carried_patients, &"treated_patients": _treated_patients, &"resting_staff": _resting_staff} + func _prepare_queues() -> void: - _queue_positions.clear() - _queue_hidden.clear() - _landing_badges.clear() - _transport_report.clear() + var model := _observe_queues() + _queue_positions = model[&"queue_positions"] + _queue_hidden = model[&"queue_hidden"] + _landing_badges.assign(model[&"landing_badges"]) + _transport_report = model[&"elevators"] + + +func _observe_queues() -> Dictionary: + var _queue_positions: Dictionary = {} + var _queue_hidden: Dictionary = {} + var _landing_badges: Array[Dictionary] = [] + var _transport_report: Dictionary = {} var groups: Dictionary = {} var facilities: Variant = _service(&"facility_service") var guests: Variant = _service(&"guest_service") @@ -877,7 +1091,8 @@ func _prepare_queues() -> void: var lift_cells: Dictionary = {} for shaft: Dictionary in _transport_report.get(&"shafts", []): for stop: Vector3i in _cells_from_serialized(shaft.get(&"stops", [])): - lift_cells[stop] = true + for offset: int in range(maxi(int(shaft.get(&"width", 2)), 1)): + lift_cells[stop + Vector3i(offset, 0, 0)] = true var reserved: Dictionary = {} var cells: Array = groups.keys() cells.sort_custom(func(a: Vector3i, b: Vector3i) -> bool: return a.y < b.y or (a.y == b.y and a.x < b.x)) @@ -892,6 +1107,8 @@ func _prepare_queues() -> void: var next := cell + Vector3i(direction*distance, 0, 0) if world == null or not world.is_walkable(next): break + if lift_cells.has(next): + break var occupant: StringName = world.get_occupant(next) var facility: Variant = facilities.get_instance(occupant) if facilities != null else null if facility != null and facility.definition_id not in [&"guest_corridor", &"service_corridor"]: @@ -908,10 +1125,10 @@ func _prepare_queues() -> void: index += 1 for hidden: int in range(index, members.size()): _queue_hidden[members[hidden]] = true - var text := "待ち%d人" % members.size() - if index < members.size(): - text += "(他%d人)" % (members.size()-index) - _landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text}) + var text := RoomPresentation.queue_label(members.size(), members.size()-index) + _landing_badges.append({&"at": get_cell_rect(cell).position + Vector2(-80 if lift_cells.has(cell) else 0, -16), &"text": text, &"cell": cell, &"members": members.duplicate(), &"visible_count": index, &"overflow": members.size()-index}) + + return {&"queue_positions": _queue_positions, &"queue_hidden": _queue_hidden, &"landing_badges": _landing_badges, &"elevators": _transport_report, &"shaft_footprint": lift_cells.keys()} func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void: @@ -1038,15 +1255,20 @@ func _draw_facility_labels() -> void: draw_string(ThemeDB.fallback_font,at+Vector2(4,12),label,HORIZONTAL_ALIGNMENT_CENTER,label_width-8,font_size,Color("f0e2b9")) -func _draw_elevators() -> void: - _boarded_actor_ids.clear() - var transport: Variant = _service(&"elevators") - if transport == null or not transport.has_method(&"get_report"): - _elevator_tracks.clear() - return - var report_value: Variant = _transport_report - if not report_value is Dictionary: - return +func _draw_elevators(canvas: CanvasItem) -> void: + for row: Dictionary in _life_observation.get(&"cabins", []): + PixelArt.elevator_cabin(canvas, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"]) + PixelArt.elevator_gate(canvas, row[&"rect"], row[&"pixel"], row[&"doors_open"]) + _draw_readable_badge(row[&"badge_at"], row[&"badge"], canvas) + for badge: Dictionary in _life_observation.get(&"landing_badges", []): + _draw_readable_badge(badge[&"at"], badge[&"text"], canvas) + + +func _observe_elevators(observation: Dictionary) -> Array[Dictionary]: + var cabins: Array[Dictionary] = [] + var _boarded_actor_ids: Dictionary = observation[&"boarded"] + var _elevator_tracks: Dictionary = observation[&"elevator_tracks"] + var report_value: Dictionary = observation[&"elevators"] var alive: Dictionary = {} var p := _cell_dimensions().x/16.0 for value: Variant in report_value.get(&"shafts", []): @@ -1062,7 +1284,7 @@ func _draw_elevators() -> void: for actor_id: Variant in passengers: _boarded_actor_ids[_elevator_actor_key(String(actor_id))] = true var floor_value := float(shaft.get(&"visual_floor", shaft.get(&"current_floor", stops[0].y))) - var visual_floor := _tracked_elevator_floor(shaft_id, floor_value) + var visual_floor := _tracked_elevator_floor(shaft_id, floor_value, observation) if stops[0].z != layer_mode: continue var origin := get_cell_rect(stops[0]) @@ -1074,10 +1296,9 @@ func _draw_elevators() -> void: var operational := bool(shaft.get(&"operational", true)) var phase := StringName(shaft.get(&"phase", &"IDLE")) var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"] - PixelArt.elevator_cabin(self,cabin,p,layer_mode==LayerMode.BACK,operational) + var passengers_drawn: Array[Dictionary] = [] var seat_columns := maxi(int((cabin.size.x / p - 4.0) / 8.0), 1) - var visible_count := mini(passengers.size(), seat_columns*2) - for index: int in range(visible_count): + for index: int in range(passengers.size()): var actor_key := _elevator_actor_key(String(passengers[index])) var is_staff := actor_key.begins_with("staff/") var person: Variant = null @@ -1088,24 +1309,23 @@ func _draw_elevators() -> void: var carrying := is_staff and person != null and not String(person.carried_inventory_id).is_empty() var state := int(person.state) if person != null else 0 var x := cabin.position.x + (2.0 + (index % seat_columns + 0.5)*8.0)*p - var feet := cabin.end.y - (2.0 + int(index / seat_columns)*9.0)*p + var feet := cabin.end.y - (2.0 + (int(index / seat_columns) % 2)*9.0)*p var center := Vector2(x, feet+3*p-_cell_dimensions().y*0.5) - _draw_pixel_actor(center,actor_key,is_staff,state,false,carrying,&"",true) - PixelArt.elevator_gate(self,cabin,p,doors_open) - var capacity := int(shaft.get(&"capacity", 0)) - var load_text := "%d/%d人" % [passengers.size(), capacity] - if capacity > 0 and passengers.size() >= capacity: - load_text = "満員 " + load_text - if not operational: - load_text = "停止 " + load_text - _draw_readable_badge(Vector2(cabin.position.x, cabin.position.y-14), load_text) - for badge: Dictionary in _landing_badges: - _draw_readable_badge(badge[&"at"], badge[&"text"]) + passengers_drawn.append(_actor_drawing(center,actor_key,is_staff,state,false,carrying,&"",true,observation)) + cabins.append({&"shaft_id": shaft_id, &"rect": cabin, &"pixel": p, + &"operational": operational, &"doors_open": doors_open, + &"phase": phase, &"passenger_count": passengers.size(), + &"passengers": passengers_drawn, + &"badge_at": Vector2(cabin.position.x, cabin.position.y-14), + &"badge_rect": _readable_badge_rect(Vector2(cabin.position.x, cabin.position.y-14), RoomPresentation.elevator_label(shaft)), + &"badge": RoomPresentation.elevator_label(shaft)}) for shaft_id: Variant in _elevator_tracks.keys(): if not alive.has(shaft_id): _elevator_tracks.erase(shaft_id) + return cabins + func _elevator_actor_key(actor_id: String) -> String: if actor_id.begins_with("guest:"): @@ -1115,7 +1335,8 @@ func _elevator_actor_key(actor_id: String) -> String: return actor_id -func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> float: +func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observation: Dictionary = {}) -> float: + var _elevator_tracks: Dictionary = observation.get(&"elevator_tracks", self._elevator_tracks) if not _elevator_tracks.has(shaft_id): _elevator_tracks[shaft_id] = {&"floor":current_floor,&"from":current_floor,&"at":_animation_time} var track: Dictionary = _elevator_tracks[shaft_id] @@ -1130,7 +1351,20 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> floa return lerpf(float(track[&"from"]),current_floor,t) -func _draw_actors() -> void: +func _draw_actors(canvas: CanvasItem = null) -> void: + for row: Dictionary in _life_observation.get(&"actor_drawings", []): + _draw_observed_actor(row, canvas) + + +func _observe_actors(observation: Dictionary) -> Array[Dictionary]: + var rows: Array[Dictionary] = [] + var _actor_tracks: Dictionary = observation[&"actor_tracks"] + var _boarded_actor_ids: Dictionary = observation[&"boarded"] + var _queue_hidden: Dictionary = observation[&"queue_hidden"] + var _queue_positions: Dictionary = observation[&"queue_positions"] + var _carried_patients: Dictionary = observation[&"carried_patients"] + var _treated_patients: Dictionary = observation[&"treated_patients"] + var _resting_staff: Dictionary = observation[&"resting_staff"] var counts: Dictionary = {} var alive: Dictionary = {} var service_slots: Dictionary = {} @@ -1144,18 +1378,18 @@ func _draw_actors() -> void: if _boarded_actor_ids.has(key) or _queue_hidden.has(key): _actor_tracks.erase(key) continue - var position := _tracked_actor_position(key,guest.current_cell) + var position := _tracked_actor_position(key,guest.current_cell,observation) if guest.current_cell.z==layer_mode: var receiving := not String(guest.current_service_id).is_empty() if _queue_positions.has(key): position = _queue_positions[key] elif receiving: - position = _service_use_position(guest,position,service_slots) + position = _service_use_position(guest,position,service_slots,observation) if not position.is_finite(): continue else: position += _actor_offset(guest.current_cell,counts) - _draw_pixel_actor(position,key,false,int(guest.state),receiving,false,_guest_activity(guest)) + rows.append(_actor_drawing(position,key,false,int(guest.state),receiving,false,_guest_activity(guest),false,observation)) var staff_service: Variant = _service(&"staff_service") if staff_service != null and staff_service.has_method(&"get_staff_members"): for staff: Variant in staff_service.call(&"get_staff_members"): @@ -1166,27 +1400,32 @@ func _draw_actors() -> void: if _boarded_actor_ids.has(key) or _queue_hidden.has(key): _actor_tracks.erase(key) continue - var position := _tracked_actor_position(key,staff.current_cell) + var position := _tracked_actor_position(key,staff.current_cell,observation) if staff.current_cell.z==layer_mode: + if _queue_positions.has(key): + rows.append(_actor_drawing(_queue_positions[key],key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation)) + continue if _carried_patients.has(staff.staff_id): var carrier: Variant = staff_service.get_staff(_carried_patients[staff.staff_id]) if carrier != null: - position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell) - Vector2(0, 12*_cell_dimensions().x/16.0) - _draw_pixel_actor(position,key,true,int(staff.state),false,false) + position = _tracked_actor_position("staff/"+String(carrier.staff_id), carrier.current_cell,observation) - Vector2(0, 12*_cell_dimensions().x/16.0) + rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation)) continue if _resting_staff.has(staff.staff_id): - position = _resting_staff_position(staff.staff_id, position) - if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false) + position = _resting_staff_position(staff.staff_id, position,observation) + if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation)) continue if _treated_patients.has(staff.staff_id): - position = _rescue_treatment_position(staff.staff_id, position) - if position.is_finite(): _draw_pixel_actor(position,key,true,int(staff.state),false,false) + position = _rescue_treatment_position(staff.staff_id, position,observation) + if position.is_finite(): rows.append(_actor_drawing(position,key,true,int(staff.state),false,false,&"",false,observation)) continue - _draw_pixel_actor(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts))),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff)) + rows.append(_actor_drawing(_queue_positions.get(key, _staff_work_position(staff, position+_actor_offset(staff.current_cell,counts),observation)),key,true,int(staff.state),false,not String(staff.carried_inventory_id).is_empty(),_staff_work_animation(staff),false,observation)) for key: Variant in _actor_tracks.keys(): if not alive.has(key): _actor_tracks.erase(key) + return rows + func _staff_work_animation(staff: StaffRuntime) -> StringName: if staff.role_id != &"cleaner": @@ -1201,7 +1440,8 @@ func _staff_work_animation(staff: StaffRuntime) -> StringName: return &"idle" -func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2: +func _staff_work_position(staff: Variant, fallback: Vector2, observation: Dictionary = {}) -> Vector2: + var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations) if staff.state not in [StaffRuntime.State.WORKING, StaffRuntime.State.AVAILABLE]: return fallback var service: Variant = _service(&"facility_service") @@ -1227,7 +1467,8 @@ func _staff_work_position(staff: Variant, fallback: Vector2) -> Vector2: return Vector2(rect.position.x+(slot+0.5)*rect.size.x/columns, rect.position.y+26*p-_cell_dimensions().y*0.5) -func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2: +func _tracked_actor_position(key: String, cell: Vector3i, observation: Dictionary = {}) -> Vector2: + var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks) if not _actor_tracks.has(key): _actor_tracks[key] = {&"cell":cell,&"from":cell,&"at":_animation_time,&"direction":1.0} var track: Dictionary = _actor_tracks[key] @@ -1246,7 +1487,8 @@ func _tracked_actor_position(key: String, cell: Vector3i) -> Vector2: return cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(cell),t) -func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary) -> Vector2: +func _service_use_position(guest: Variant, fallback: Vector2, _slots: Dictionary, observation: Dictionary = {}) -> Vector2: + var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations) # Guest paths stop at an access cell. While a service is active, project the # occupant into that facility's use position; authoritative paths stay intact. var id := StringName(guest.target_facility_id) @@ -1281,6 +1523,21 @@ func _guest_activity(guest: Variant) -> StringName: func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false) -> void: + _draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding)) + + +func _draw_observed_actor(row: Dictionary, canvas: CanvasItem = null) -> void: + if canvas == null: + canvas = self + var at: Vector2 = row[&"at"] + var p: float = row[&"scale"] + canvas.draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2)) + People.draw(canvas, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"]) + + +func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, receiving: bool, carrying: bool, activity: StringName = &"", riding := false, observation: Dictionary = {}) -> Dictionary: + var _actor_tracks: Dictionary = observation.get(&"actor_tracks", self._actor_tracks) + var _queue_positions: Dictionary = observation.get(&"queue_positions", self._queue_positions) var world_pixel := _cell_dimensions().x / 16.0 var scale := 0.5 if riding else 0.75 var p := maxf(floorf(world_pixel * scale), 1.0) if world_pixel >= 2.0 else world_pixel * scale @@ -1307,8 +1564,274 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re elif _queue_positions.has(key) or (not staff and state == GuestRuntime.State.QUEUING): mode = &"wait" var at := (center + Vector2(-8 * p, _cell_dimensions().y * 0.5 - 3 * world_pixel - 17 * p)).round() - draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2)) - People.draw(self, at, p, mode, frame, variant, staff, float(track.get(&"direction", 1.0)) < 0.0) + return {&"key": key, &"center": center, &"at": at, &"scale": p, &"mode": mode, + &"frame": frame, &"variant": variant, &"staff": staff, &"state": state, + &"receiving": receiving, &"carrying": carrying, &"activity": activity, + &"riding": riding, &"flip": float(track.get(&"direction", 1.0)) < 0.0} + + +## Identity cores are tied to the unchanged PixelPeople poses, not sprite boxes. +func _life_core(row: Dictionary) -> Rect2: + var phase := int(row[&"frame"]) % 8 + var core := Rect2(6, 1, 6, 12) + if row[&"mode"] in [&"sleep", &"collapsed", &"standard_room_sleep"]: + core = Rect2(2, 11, 13, 5) + if row[&"mode"] == &"standard_room_sleep" and phase in [2, 3, 4]: + core.position.y -= 1 + core.size.y += 1 + elif row[&"mode"] in [&"walk", &"carry_walk"] and phase in [2, 6]: + core.position.y -= 1 + if row[&"flip"]: + core.position.x = 16-core.end.x + return Rect2(row[&"at"]+core.position*row[&"scale"], core.size*row[&"scale"]) + + +func _life_track_region(key: String, cell: Vector3i, model: Dictionary) -> Dictionary: + var track: Dictionary = model[&"actor_tracks"].get(key, {}) + var transit: bool = not model[&"paused"] and track.get(&"from", cell) != cell and _animation_time-float(track.get(&"at", 0.0)) < 0.42 + var rect := get_cell_rect(cell) + if transit: + rect = rect.merge(get_cell_rect(track[&"from"])) + return {&"bounds": rect, &"transit": transit} + + +## Keep non-passengers on a real corridor landing when a cabin crosses their pose. +func _life_landing_region(region: Dictionary, cell: Vector3i, model: Dictionary) -> Dictionary: + var blocked := false + for cabin: Dictionary in model[&"cabins"]: + if (region[&"bounds"] as Rect2).intersects(cabin[&"rect"]): + blocked = true + break + if not blocked: + return region + var world: Variant = _service(&"world_service") + var facilities: Variant = _service(&"facility_service") + var footprint: Array = model[&"shaft_footprint"] + var origins: Array = footprint.filter(func(c: Vector3i) -> bool: return c.y == cell.y and c.z == cell.z) + origins.sort_custom(func(a: Vector3i, b: Vector3i) -> bool: return absi(a.x-cell.x) < absi(b.x-cell.x) or (absi(a.x-cell.x) == absi(b.x-cell.x) and a.x < b.x)) + var lane: Array[Vector3i] = [] + for origin: Vector3i in origins: + for direction: int in [-1, 1]: + for distance: int in range(1, 9): + var target := origin + Vector3i(direction*distance, 0, 0) + if world == null or footprint.has(target) or not world.is_walkable(target): + break + var facility: Variant = facilities.get_instance(world.get_occupant(target)) if facilities != null else null + if facility != null and facility.definition_id not in [&"guest_corridor", &"service_corridor"]: + break + if not lane.has(target): + lane.append(target) + lane.sort_custom(func(a: Vector3i, b: Vector3i) -> bool: return absi(a.x-cell.x) < absi(b.x-cell.x) or (absi(a.x-cell.x) == absi(b.x-cell.x) and a.x < b.x)) + if lane.is_empty(): + var anchor := get_cell_rect(cell).position+Vector2(-80, -16) + var nearest := INF + for badge: Dictionary in model[&"landing_badges"]: + var landing: Vector3i = badge[&"cell"] + if landing.y == cell.y and landing.z == cell.z and absf(landing.x-cell.x) < nearest: + nearest = absf(landing.x-cell.x) + anchor = badge[&"at"] + return {&"bounds": region[&"bounds"], &"transit": false, &"no_lane": true, &"badge_at": anchor} + var selected: Vector3i = lane[0] + for target: Vector3i in lane: + if not model[&"queue_positions"].values().has(cell_to_canvas(target)): + selected = target + break + return {&"bounds": get_cell_rect(selected), &"transit": false, &"center": cell_to_canvas(selected)} + + +func _life_actor_region(row: Dictionary, person: Dictionary, model: Dictionary) -> Dictionary: + var key: String = row[&"key"] + var cell: Vector3i = person[&"cell"] + var result := _life_track_region(key, cell, model) + var p := _cell_dimensions().x/16.0 + var world: Variant = _service(&"world_service") + var facility_id: StringName = &"" + if model[&"queue_positions"].has(key): + result = {&"bounds": Rect2(model[&"queue_positions"][key]-_cell_dimensions()*0.5, _cell_dimensions()), &"transit": false} + elif not row[&"staff"]: + if not String(person[&"service"]).is_empty(): + var room: Dictionary = model[&"rooms"].get(person[&"target"], {}) + if (room.get(&"people", []) as Array).has(person[&"id"]): + facility_id = person[&"target"] + else: + var id: StringName = person[&"id"] + if model[&"carried_patients"].has(id): + for carrier: Dictionary in model[&"staff"]: + if carrier[&"id"] == model[&"carried_patients"][id]: + result = _life_actor_region({&"key": "staff/"+String(carrier[&"id"]), &"staff": true}, carrier, model) + var rect: Rect2 = result[&"bounds"] + rect = rect.grow(p) + rect.position.y -= 12*p + result[&"bounds"] = rect + if result.has(&"center"): + result[&"center"] -= Vector2(0, 12*p) + elif model[&"resting_staff"].has(id): + facility_id = model[&"resting_staff"][id] + elif model[&"treated_patients"].has(id): + facility_id = model[&"treated_patients"][id] + elif person[&"state"] in ["WORKING", "AVAILABLE"] and world != null: + var cells: Array[Vector3i] = world.get_entity_cells(person[&"facility"]) + var room: Dictionary = model[&"rooms"].get(person[&"facility"], {}) + var slot := (room.get(&"staff", []) as Array).find(id) + var rect := _cell_bounds_rect(cells) + var columns := maxi(int(rect.size.x/(16*p)), 1) + if cells.has(cell) and slot >= 0 and slot < columns and rect.size.y >= 40*p: + result = {&"bounds": Rect2(rect.position+Vector2(slot*rect.size.x/columns, 0), Vector2(rect.size.x/columns, rect.size.y)), &"transit": false} + if not facility_id.is_empty() and world != null: + var cells: Array[Vector3i] = world.get_entity_cells(facility_id) + if not cells.is_empty(): + result = {&"bounds": _cell_bounds_rect(cells), &"transit": false} + if not model[&"queue_positions"].has(key) and not result.has(&"center") and not result.get(&"no_lane", false): + result = _life_landing_region(result, cell, model) + result[&"bounds"] = (result[&"bounds"] as Rect2).grow(-p) + return result + + +func _life_scaled(row: Dictionary, scale: float) -> Dictionary: + var result := row.duplicate(true) + result[&"scale"] = scale + result[&"at"] = (row[&"center"]+Vector2(-8*scale, _cell_dimensions().y*0.5-3*_cell_dimensions().x/16.0-17*scale)).round() + return result + + +## Forward/backward packing keeps each member's own interval. No relocation. +func _life_pack(group: Array, scale: float) -> Array: + var trial: Array = [] + var right := -INF + for member: Dictionary in group: + var row := _life_scaled(member[&"row"], minf(scale, member[&"row"][&"scale"])) + var core := _life_core(row) + var bounds: Rect2 = member[&"bounds"] + var x := maxf(maxf(core.position.x, bounds.position.x), right) + row[&"at"].x += x-core.position.x + right = x+core.size.x + trial.append(row) + var left := INF + for i: int in range(trial.size()-1, -1, -1): + var row: Dictionary = trial[i] + var core := _life_core(row) + var bounds: Rect2 = group[i][&"bounds"] + var x := minf(core.position.x, minf(bounds.end.x, left)-core.size.x) + row[&"at"].x += x-core.position.x + left = x + if x < bounds.position.x or core.position.y < bounds.position.y or core.end.y > bounds.end.y: + return [] + return trial + + +## Rebuild vertical components at every fitted scale. Ceilings only decrease; +## once none decreases, the feet/cores and therefore the partition are fixed. +func _life_fitted_groups(members: Array) -> Array: + var groups: Array = [] + while true: + for member: Dictionary in members: + member[&"row"].merge(_life_scaled(member[&"row"], member[&"row"][&"scale"]), true) + members.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + var ay := _life_core(a[&"row"]).position.y + var by := _life_core(b[&"row"]).position.y + return ay < by or (ay == by and a[&"row"][&"key"] < b[&"row"][&"key"])) + groups = [] + var bottom := -INF + for member: Dictionary in members: + var core := _life_core(member[&"row"]) + if groups.is_empty() or core.position.y >= bottom: + groups.append([]) + bottom = core.end.y + else: + bottom = maxf(bottom, core.end.y) + groups.back().append(member) + var reduced := false + for group: Array in groups: + group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"])) + var ceiling := 1.0 + for member: Dictionary in group: + ceiling = maxf(ceiling, member[&"row"][&"scale"]) + if _life_pack(group, ceiling).is_empty() and ceiling > 1: + for member: Dictionary in group: + var scale := minf(member[&"row"][&"scale"], maxf(1, int(ceiling)-1)) + member[&"row"].merge(_life_scaled(member[&"row"], scale), true) + reduced = true + if not reduced: + return groups + return groups + + +func _fit_life_actors(model: Dictionary) -> void: + var people: Dictionary = {} + for person: Dictionary in model[&"guests"]: + people["guest/"+String(person[&"id"])] = person + for person: Dictionary in model[&"staff"]: + people["staff/"+String(person[&"id"])] = person + var members: Array = [] + var stranded: Array = [] + var all_rows: Array = model[&"actor_drawings"].duplicate() + var passenger_bounds: Dictionary = {} + for cabin: Dictionary in model[&"cabins"]: + for row: Dictionary in cabin[&"passengers"]: + all_rows.append(row) + passenger_bounds[row[&"key"]] = (cabin[&"rect"] as Rect2).grow(-_cell_dimensions().x/16.0) + for row: Dictionary in all_rows: + var person: Dictionary = people[row[&"key"]] + row[&"cell"] = person[&"cell"] + var region := _life_actor_region(row, person, model) if not row[&"riding"] else {&"bounds": passenger_bounds[row[&"key"]], &"transit": false} + if region.has(&"center"): + var jitter := Vector2.ZERO + if not model[&"carried_patients"].has(person[&"id"]): + var track: Dictionary = model[&"actor_tracks"][row[&"key"]] + var t := 1.0 if model[&"paused"] else clampf((_animation_time-float(track[&"at"]))/0.42, 0, 1) + jitter = row[&"center"]-cell_to_canvas(track[&"from"]).lerp(cell_to_canvas(person[&"cell"]), t) + row[&"center"] = region[&"center"]+jitter + row.merge(_life_scaled(row, row[&"scale"]), true) + region[&"row"] = row + if region.get(&"no_lane", false): + stranded.append(region) + elif not region[&"transit"]: + members.append(region) + var groups := _life_fitted_groups(members) + model[&"actor_aggregations"] = [] + var hidden: Dictionary = {} + for member: Dictionary in stranded: + var row: Dictionary = member[&"row"] + hidden[row[&"key"]] = true + var aggregate: Dictionary = {} + for existing: Dictionary in model[&"actor_aggregations"]: + if existing[&"at"] == member[&"badge_at"]: + aggregate = existing + break + if aggregate.is_empty(): + aggregate = {&"members": [], &"drawn_keys": [], &"at": member[&"badge_at"], &"text": ""} + model[&"actor_aggregations"].append(aggregate) + aggregate[&"members"].append({&"key": row[&"key"], &"id": people[row[&"key"]][&"id"], &"state": row[&"state"], &"cell": row[&"cell"], &"mode": row[&"mode"]}) + aggregate[&"text"] = "+%d" % aggregate[&"members"].size() + for group: Array in groups: + group.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x or (a[&"row"][&"center"].x == b[&"row"][&"center"].x and a[&"row"][&"key"] < b[&"row"][&"key"])) + var base := 1.0 + for member: Dictionary in group: + base = maxf(base, member[&"row"][&"scale"]) + var fitted := _life_pack(group, base) + var retained: Array = group + if fitted.is_empty(): + retained = [] + var identities: Array = [] + var drawn: Array = [] + for member: Dictionary in group: + var row: Dictionary = member[&"row"] + identities.append({&"key": row[&"key"], &"id": people[row[&"key"]][&"id"], &"state": row[&"state"], &"cell": row[&"cell"], &"mode": row[&"mode"]}) + var attempt := retained.duplicate() + attempt.append(member) + var packed := _life_pack(attempt, 1) + if not packed.is_empty(): + retained = attempt + fitted = packed + drawn.append(row[&"key"]) + else: + hidden[row[&"key"]] = true + model[&"actor_aggregations"].append({&"members": identities, &"drawn_keys": drawn, &"at": (group[0][&"bounds"] as Rect2).position, &"text": "+%d" % (group.size()-retained.size())}) + for i: int in fitted.size(): + retained[i][&"row"].merge(fitted[i], true) + model[&"actor_drawings"] = model[&"actor_drawings"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"])) + for cabin: Dictionary in model[&"cabins"]: + cabin[&"passengers"] = cabin[&"passengers"].filter(func(row: Dictionary) -> bool: return not hidden.has(row[&"key"])) func _draw_excavation_jobs() -> void: @@ -1421,14 +1944,14 @@ func _facility_color(instance_id: StringName) -> Color: var instance: Variant = facility_service.call(&"get_instance", instance_id) if instance == null: return Color("6f8b67") if layer_mode == LayerMode.FRONT else Color("427582") - match int(instance.state): - 0: + match int(_room_presentations.get(instance_id, {}).get(&"state", instance.state)): + FacilityInstance.State.BROKEN: return Color("a24b45") - 1, 2, 3: + FacilityInstance.State.NO_ACCESS, FacilityInstance.State.NO_POWER, FacilityInstance.State.NO_STAFF: return Color("a77d3f") - 4: + FacilityInstance.State.CLOSED: return Color("6b6258") - 5: + FacilityInstance.State.DEGRADED: return Color("89914d") _: return Color("6f9a55") if layer_mode == LayerMode.FRONT else Color("67518e") @@ -1579,7 +2102,9 @@ func _normalized_rotation(value: int) -> int: return posmod(value, 360) / 90 * 90 -func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Vector2: +func _rescue_treatment_position(patient_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2: + var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations) + var _treated_patients: Dictionary = observation.get(&"treated_patients", self._treated_patients) var facility_id: StringName = _treated_patients[patient_id] var world: Variant = _service(&"world_service") var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else [] @@ -1592,7 +2117,9 @@ func _rescue_treatment_position(patient_id: StringName, fallback: Vector2) -> Ve return anchors[slot]+Vector2(0,-2*p-_cell_dimensions().y*0.5) -func _resting_staff_position(staff_id: StringName, fallback: Vector2) -> Vector2: +func _resting_staff_position(staff_id: StringName, fallback: Vector2, observation: Dictionary = {}) -> Vector2: + var _room_presentations: Dictionary = observation.get(&"rooms", self._room_presentations) + var _resting_staff: Dictionary = observation.get(&"resting_staff", self._resting_staff) var facility_id: StringName = _resting_staff[staff_id] var world: Variant = _service(&"world_service") var cells: Array[Vector3i] = world.get_entity_cells(facility_id) if world != null else [] diff --git a/scripts/ui/room_presentation.gd b/scripts/ui/room_presentation.gd index 5945440..0c1fa7f 100644 --- a/scripts/ui/room_presentation.gd +++ b/scripts/ui/room_presentation.gd @@ -10,7 +10,9 @@ static func describe(instance: Variant) -> Dictionary: if not ids.has(id): ids.append(id) ids.sort_custom(func(a: StringName, b: StringName) -> bool: return String(a) < String(b)) - return {&"operation": int(instance.operation_state), &"people": ids, + return {&"state": int(instance.state), &"state_name": FacilityInstance.State.keys()[instance.state], + &"reasons": instance.state_reasons.duplicate(), + &"operation": int(instance.operation_state), &"people": ids, &"count": ids.size(), &"limit": int(instance.customer_capacity), &"staff": instance.active_staff_ids.duplicate(), &"suspension": StringName(instance.suspension_reason)} @@ -50,3 +52,52 @@ static func label(data: Dictionary, visible_count: int) -> String: if count > visible_count: value += "(他%d人)" % (count - visible_count) return value + + +static func business_label(data: Dictionary) -> String: + if data.is_empty(): + return "" + if int(data[&"operation"]) == FacilityInstance.OperationState.SUSPENDED: + return "休止・摘発" if data[&"suspension"] == &"RAIDED" else "休止・破壊" + match int(data[&"state"]): + FacilityInstance.State.BROKEN: return "破損" + FacilityInstance.State.NO_ACCESS: return "通路なし" + FacilityInstance.State.NO_POWER: return "魔力なし" + FacilityInstance.State.NO_STAFF: return "職員不足" + FacilityInstance.State.CLOSED: return "営業停止" + FacilityInstance.State.DEGRADED: return "低下運転" + FacilityInstance.State.OPEN: return "営業中" + return String(data[&"state_name"]) + + +static func queue_label(count: int, overflow: int) -> String: + return "待ち%d人" % count + ("(他%d人)" % overflow if overflow > 0 else "") + + +static func elevator_label(shaft: Dictionary) -> String: + var count := (shaft[&"boarded"] as Array).size() + var capacity := int(shaft[&"capacity"]) + var value := "%d/%d人" % [count, capacity] + if capacity > 0 and count >= capacity: + value = "満員 " + value + if not bool(shaft[&"operational"]): + value = "停止 " + value + else: + value = "運転 " + value + var phase := String(shaft.get(&"phase", &"IDLE")) + var doors_open := bool(shaft[&"operational"]) and phase in ["IDLE", "BOARDING", "ALIGHTING"] + return value + (" 開 " if doors_open else " 閉 ") + phase + + +static func supply_label(requests: Array[Dictionary]) -> String: + # These are requests, never invented stock quantities. Preserve actual states. + var states: Dictionary = {} + for request: Dictionary in requests: + var state := String(request[&"state"]) + states[state] = int(states.get(state, 0)) + 1 + var names: Array = states.keys() + names.sort() + var parts: PackedStringArray = [] + for state: String in names: + parts.append("%s:%d" % [state, states[state]]) + return "物資依頼 " + " / ".join(parts) diff --git a/tests/fixtures/p23-03/diagnostic_stream.gd b/tests/fixtures/p23-03/diagnostic_stream.gd new file mode 100644 index 0000000..7f51e24 --- /dev/null +++ b/tests/fixtures/p23-03/diagnostic_stream.gd @@ -0,0 +1,165 @@ +extends RefCounted +## Sparse whole typed captures. No acceptance flags and no truncation. +const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd") +var directory: String +var manifest: FileAccess +var observer: MotionObserver + +class MotionObserver extends Node: + var view: WorldPreview + var frames: Array = [] + var elapsed := 0.0 + var scaled_elapsed := 0.0 + func _ready() -> void: + RenderingServer.frame_post_draw.connect(after_draw) + view.draw.connect(on_view_draw) + func _process(delta: float) -> void: + elapsed += delta + var clock := get_node_or_null("/root/GameClock") + if clock == null or not bool(clock.get("paused")): + scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0) + func on_view_draw() -> void: + # Read the draw-consumed report after the real _draw completes. + after_draw.call_deferred() + func display_sample(report: Dictionary) -> Dictionary: + var sample := {} + for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]: + sample[key] = report[key] + return sample + func after_draw() -> void: + if not is_instance_valid(view): return + var frame := Engine.get_process_frames() + if not frames.is_empty() and frame <= int(frames[-1]["frame"]): return + var report := view.get_life_observation_report() + if report.is_empty(): return + var sample := display_sample(report) + if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return + # Keep only two draw frames, never whole Runtime copies per frame. + frames.append({"frame": frame, "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample}) + if frames.size() > 2: frames.pop_front() + elapsed = 0.0 + scaled_elapsed = 0.0 + func _exit_tree() -> void: + if is_instance_valid(view) and view.draw.is_connected(on_view_draw): view.draw.disconnect(on_view_draw) + if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw) + +func watch(view: WorldPreview) -> void: + observer = MotionObserver.new() + observer.view = view + view.add_child(observer) + +func await_draws(tree: SceneTree, view: WorldPreview, n: int = 2, bound: int = 120) -> bool: + var start := Engine.get_process_frames() + for unused: int in range(bound): + view.queue_redraw() + await tree.process_frame + if not is_instance_valid(observer): break + var report := view.get_life_observation_report() + if observer.frames.size() == n and not report.is_empty() and int(observer.frames[0]["frame"]) > start and observer.frames[-1]["display"] == observer.display_sample(report): + return true + # Persist timeout as a failure, never a skip. + record("draw-history-timeout", {"start_frame": start, "bound": bound, "required_frames": n}) + return false + +func open(path: String) -> bool: + directory = path + if DirAccess.dir_exists_absolute(path): + return false + if DirAccess.make_dir_recursive_absolute(path.path_join("objects")) != OK: + return false + manifest = FileAccess.open(path.path_join("manifest.jsonl"), FileAccess.WRITE) + return manifest != null + +func object(value: Variant) -> String: + var text := JSON.stringify(Codec.new().typed(value)) + "\n" + var digest := text.sha256_text() + var path := directory.path_join("objects").path_join(digest + ".json") + if FileAccess.file_exists(path): + return digest if FileAccess.get_sha256(path) == digest else "" + var file := FileAccess.open(path, FileAccess.WRITE) + if file == null: + return "" + file.store_string(text) + file.close() + return digest if FileAccess.get_sha256(path) == digest else "" + +func capture(label: String, runtime: Variant, view: WorldPreview) -> bool: + if manifest == null: + return false + var state: Dictionary = runtime.create_snapshot() + var report := view.get_life_observation_report() + var state_hash := object(state) + var report_hash := object(report) + var audit_hash := object(rest_audit(runtime)) + var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []}) + var after: Dictionary = runtime.create_snapshot() + var after_hash := object(after) + if state_hash.is_empty() or report_hash.is_empty() or audit_hash.is_empty(): + return false + manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(), + "ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash, + "rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash, + "input_method": "diagnostic", "acceptance": "pending"})) + manifest.flush() + return var_to_bytes(state) == var_to_bytes(after) + +func close() -> void: + if manifest != null: + manifest.close() + manifest = null + +# Public selection inputs; Runtime output is only a comparison operand. +func rest_audit(runtime: Variant) -> Dictionary: + var rows: Array = [] + for member: StaffRuntime in runtime.staff_service.get_staff_members(): + var memberships: Array = [] + for raid_id: StringName in runtime.defense_service.get_raid_ids(): + var raid: Dictionary = runtime.defense_service.get_raid(raid_id) + if raid.get(&"combatants", {}).has(member.staff_id): + memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]], + "side": raid[&"combatants"][member.staff_id].get(&"side", &"")}) + var facilities: Array = [] + for room: FacilityInstance in runtime.facility_service.get_instances(): + var probes: Array = [] + var candidates: Dictionary = {} + var occupied := room.occupied_cells.duplicate() + if occupied.is_empty(): occupied.append(room.origin) + for cell: Vector3i in occupied: + for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]: + var present: bool = runtime.path_service.has_cell(cell + delta) + probes.append({"cell": cell + delta, "has_cell": present}) + if present: candidates[cell + delta] = true + var ordered: Array = candidates.keys() + ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool: + if a.z != b.z: return a.z < b.z + if a.y != b.y: return a.y < b.y + return a.x < b.x) + var routes: Array = [] + for cell: Vector3i in ordered: + var path: Array = runtime.path_service.find_path(member.current_cell, cell, + [WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"]) + var edges: Array = [] + for index: int in range(1, path.size()): + # Empty reports remain explicit mismatch evidence. + edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"])) + routes.append({"cell": cell, "path": path, "edges": edges, + "calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1}) + facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id, + "operation_state": FacilityInstance.OperationState.keys()[room.operation_state], + "origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(), + "probes": probes, "ordered": ordered, "routes": routes}) + rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state], + "health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell, + "defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id), + "in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities, + "comparison_only": runtime.get_staff_rest_report(member.staff_id)}) + return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")} + +func record(label: String, value: Dictionary) -> bool: + if manifest == null: return false + var digest := object(value) + if digest.is_empty(): return false + manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(), + "ticks_usec": Time.get_ticks_usec(), "object": digest})) + manifest.flush() + return true diff --git a/tests/fixtures/p23-03/diagnostic_stream.gd.uid b/tests/fixtures/p23-03/diagnostic_stream.gd.uid new file mode 100644 index 0000000..0140e88 --- /dev/null +++ b/tests/fixtures/p23-03/diagnostic_stream.gd.uid @@ -0,0 +1 @@ +uid://d23lifestr03 diff --git a/tests/fixtures/p23-03/layout_comparison.gd b/tests/fixtures/p23-03/layout_comparison.gd new file mode 100644 index 0000000..4a38945 --- /dev/null +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -0,0 +1,102 @@ +extends RefCounted +## Public-command experiment; no synthetic state, costs or capacities. +## Predeclared metric: total destination-arrival minutes of the entire cohort. +## B must be strictly smaller. Incomplete cohorts cannot establish improvement. +const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd") +const SEED := 806042 +const WINDOW := 480 +const START := Vector3i(2, 0, 0) +const GOAL := Vector3i(2, 1, 0) +var stream: RefCounted + +func declaration() -> Dictionary: + var rng := RandomNumberGenerator.new() + rng.seed = SEED + var arrivals: Array[Dictionary] = [] + for index: int in range(48): + arrivals.append({&"id": StringName("p23-%03d" % index), + &"offset": (index / 4) * 15 + rng.randi_range(0, 2)}) + arrivals.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + return a[&"offset"] < b[&"offset"] or (a[&"offset"] == b[&"offset"] and String(a[&"id"]) < String(b[&"id"]))) + return {&"seed": SEED, &"initial_clock": [1, 6, 0], &"budget": &"GameState.reset", + &"production": &"Runtime.initialize defaults; production operations not enabled", + &"arrivals": arrivals, &"from": START, &"to": GOAL, &"window": WINDOW, + &"metric": &"sum_destination_arrival_minutes_entire_cohort", + &"improvement": {&"definition_id": &"guest_elevator", &"origin": Vector3i(4, 0, 0)}} + +func run(tree: SceneTree, directory: String, layout: String) -> bool: + if layout not in ["A", "B"]: return false + var global_clock := tree.root.get_node_or_null("GameClock") + if global_clock != null: global_clock.set_paused(true) + stream = Stream.new() + if not stream.open(directory): return false + var input := declaration() + if not stream.record("declaration", input): + stream.close() + return false + var runtime: Variant = load("res://autoload/simulation_runtime.gd").new() + var clock: Variant = load("res://autoload/game_clock.gd").new() + var state: Variant = load("res://autoload/game_state.gd").new() + clock.reset(1, 6, 0) + state.reset() + var ok: bool = runtime.initialize(clock, state, SEED)[&"valid"] + clock.set_paused(true) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + ok = stream.record("initial", runtime.create_snapshot()) and ok + var builds: Array = [[&"guest_elevator", Vector3i.ZERO], + [&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)]] + if layout == "B": builds.append([input[&"improvement"][&"definition_id"], input[&"improvement"][&"origin"]]) + var construction_minutes := 0 + # Identical preparation time derived from the same registered definitions. + for id: StringName in [&"guest_elevator", &"guest_corridor"]: + construction_minutes = maxi(construction_minutes, runtime.definition_registry.get_facility(id).build_minutes) + for index: int in range(builds.size()): + var payload := {&"definition_id": builds[index][0], &"origin": builds[index][1]} + var result: CommandResult = runtime.submit_command(&"build_facility", payload, + &"player", &"", 0, clock.absolute_minute, StringName("p23.build.%d" % index)) + ok = stream.record("build-%d" % index, {&"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok + clock.advance_minutes(construction_minutes + 1) + ok = stream.record("prepared", runtime.create_snapshot()) and ok + runtime.domain_events.subscribe(&"*", &"p23.capture", Callable(self, "_event")) + var view := WorldPreview.new() + view.size = Vector2(1440, 810) + view.set_runtime(runtime) + tree.root.add_child(view) + stream.watch(view) + var finished: Dictionary = {} + for offset: int in range(WINDOW + 1): + if not ok: break + if offset > 0: clock.advance_minutes(1) + for arrival: Dictionary in input[&"arrivals"]: + if arrival[&"offset"] != offset: continue + var payload := {&"guest_id": arrival[&"id"], &"guest_type_id": &"traveler", &"gold": 100, + &"options": {&"current_cell": START, &"state": GuestRuntime.State.MOVING, + &"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000, + &"path_handle": {&"target_cell": GOAL}}} + var result: CommandResult = runtime.submit_command(&"spawn_guest", payload, + &"world", arrival[&"id"], 10, -1, StringName("p23.spawn.%s" % arrival[&"id"])) + ok = stream.record("arrival-%s" % arrival[&"id"], {&"offset": offset, &"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok + # Typed per-minute projections retain paths, queues, density and actual + # completed services. Whole aggregate snapshots are only sampled sparsely. + var guest_records: Dictionary = runtime.guest_service.create_snapshot() + ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute, + &"guests": guest_records, &"path": runtime.path_service.create_snapshot(), + &"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok + for guest: Dictionary in guest_records[&"guests"]: + if guest[&"current_cell"] != [GOAL.x, GOAL.y, GOAL.z] or finished.has(guest[&"guest_id"]): continue + finished[guest[&"guest_id"]] = offset + if offset % 30 == 0: + var drawn: bool = await stream.await_draws(tree, view) + ok = drawn and stream.capture("frame-%03d" % offset, runtime, view) and ok + ok = stream.record("first-arrivals", {&"offsets": finished}) and ok + ok = stream.record("final", runtime.create_snapshot()) and ok + stream.close() + view.free() + runtime.free() + clock.free() + state.free() + return ok + +func _event(event: SimulationDomainEvent) -> void: + stream.record("event", {&"type": event.event_type, &"minute": event.absolute_minute, + &"entities": event.entity_ids, &"payload": event.payload.duplicate(true)}) diff --git a/tests/fixtures/p23-03/layout_comparison.gd.uid b/tests/fixtures/p23-03/layout_comparison.gd.uid new file mode 100644 index 0000000..64f64f2 --- /dev/null +++ b/tests/fixtures/p23-03/layout_comparison.gd.uid @@ -0,0 +1 @@ +uid://dtxpm0n6cfom diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd new file mode 100644 index 0000000..e65bfac --- /dev/null +++ b/tests/fixtures/p23-03/observation.gd @@ -0,0 +1,193 @@ +extends "res://tests/fixtures/p08-03/placement.gd" +## Real Runtime commands; inherited formal codec/save-operation helpers unchanged. +const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd") +const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd") +const PurchaseQuoteV2 := preload("res://scripts/supply/supply_purchase_quote_v2.gd") +var stream: RefCounted +var layout := "A" +var facility_ids: Dictionary = {} + +func setup() -> void: + clock.reset(1, 6, 0) + var state: Variant = tree.root.get_node("GameState") + state.reset() + expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation") + clock.set_paused(true) + tree.root.size = Vector2i(1440, 810) + layout = OS.get_environment("P23_LAYOUT") + expect(layout in ["A", "B"], "declared layout A or B") + main = load("res://scenes/main/main.tscn").instantiate() + main.manual_save_path = "user://saves/manual_ui" + if role == "reader": + import_package(directory.path_join("formal"), String(main.manual_save_path)) + tree.root.add_child(main) + runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) + stream = Stream.new() + expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream") + stream.watch(main.world_preview) + await frames() + +func command(id: StringName, payload: Dictionary) -> bool: + var result: CommandResult = runtime.submit_command(id, payload) + expect(result.is_success(), String(id) + " " + str(payload) + ": " + str(result.code)) + expect(stream.record("command", {&"command": id, &"payload": payload, + &"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command") + return result.is_success() + +func advance(minutes: int) -> void: + for unused: int in range(minutes): + clock.advance_minutes(1) + await frames() + await observe_progress() + if clock.absolute_minute % 30 == 0: + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") + expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation") + +func observe_progress() -> void: + pass + +func prepare() -> void: + # Both layouts receive the same runtime-derived budget for all setup spends. + # Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV. + var builds: Array = [[&"reception", Vector3i(-8, 0, 0)], + [&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)], + [&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)], + [&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)], + [&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)], + [&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO], + [&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)], + [&"staff_waiting_room", Vector3i(-4, 0, 1)]] + var improvement: Dictionary = Comparison.new().declaration()[&"improvement"] + var budget_builds := builds.duplicate(true) + budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]]) + var assignments: Array = [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], + [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]] + var purchase_quantity := 40 + var needed := {&"gold": 0, &"materials": 0} + var costs: Array = [] + for row: Array in budget_builds: + var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0]) + costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)}) + for key: StringName in needed: + needed[key] += int(definition.construction_cost.get(key, 0)) + var construction_total := needed.duplicate(true) + var signing_costs: Array = [] + var signing_total := 0 + for assignment: Array in assignments: + var candidate: Dictionary = runtime.mvp_content_catalog.get_recruitment_entry(assignment[0]) + expect(candidate.has(&"signing_cost"), "registered signing cost " + String(assignment[0])) + var signing_cost := int(candidate[&"signing_cost"]) + signing_costs.append({&"candidate_id": assignment[0], &"signing_cost": signing_cost, + &"source": "MvpContentCatalog.get_recruitment_entry.signing_cost"}) + signing_total += signing_cost + needed[&"gold"] += signing_total + # Before warehouse construction, use the purchase service's catalog/quote + # branch. Check this total against its live preview immediately before buying. + var item := SupplyCatalog.get_item(SupplyCatalog.RAW_FOOD) + var purchase_quote := PurchaseQuoteV2.make(SupplyCatalog.RAW_FOOD, purchase_quantity, int(item[&"unit_price"])) + expect(not purchase_quote.is_empty(), "registered raw-food purchase quote") + var purchase_total := int(purchase_quote[&"effective_total"] if PurchaseQuoteV2.eligible(runtime) else purchase_quote[&"base_total"]) + needed[&"gold"] += purchase_total + var purchase_cost := {&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": purchase_quantity, + &"unit_price": item[&"unit_price"], &"quote": purchase_quote, &"total_cost": purchase_total, + &"source": "SupplyCatalog.unit_price / PurchaseQuoteV2.make; preview_purchase eligibility"} + var state: Variant = tree.root.get_node("GameState") + var deltas := {} + for key: StringName in needed: + if needed[key] > state.get_resource(key): + deltas[key] = needed[key] - state.get_resource(key) + budget = {"setup": "diagnostic", "costs": costs, "needed": needed, + "initial": state.resources.duplicate(true), "deltas": deltas, + "construction_total": construction_total, "signing_costs": signing_costs, + "signing_total": signing_total, "purchase_cost": purchase_cost} + if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas}) + expect(stream.record("budget", budget), "common diagnostic budget") + if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]]) + # Preserve the supply scenario's staged support/construction sequence. + for index: int in range(builds.size()): + if index == 5: await advance(180) + if index == 7: await advance(60) + if index == 8: await advance(10) + command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]}) + var transport_minutes := 0 + for id: StringName in [&"guest_elevator", &"guest_corridor"]: + transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes) + await advance(maxi(10, transport_minutes)) + for facility: FacilityInstance in runtime.facility_service.get_instances(): + facility_ids[facility.definition_id] = facility.instance_id + command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"], + &"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}}) + for assignment: Array in assignments: + command(&"hire_staff_candidate", {&"candidate_id": assignment[0]}) + command(&"set_staff_work_assignment", {&"staff_id": assignment[0], + &"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"}) + var actual_quote: Dictionary = runtime.preview_supply_purchase(facility_ids[&"warehouse"], SupplyCatalog.RAW_FOOD, purchase_quantity) + expect(int(actual_quote[&"total_cost"]) == purchase_total, "budget matches actual purchase quote") + expect(stream.record("setup-purchase-quote", actual_quote), "record actual setup purchase quote") + command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"], + &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": purchase_quantity}) + command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"], + &"enabled": true, &"target_stock": 8}) + # The OS observation uses the already declared transport cohort, plus + # ordinary public arrivals whose service choice remains Runtime-owned. + # These commands are recorded and identical across A/B; no actor is moved, + # despawned, or inserted into a room/queue by the fixture. + var demand: Dictionary = Comparison.new().declaration() + expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"], + "schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison", + "minute": clock.absolute_minute}), "declared observation demand") + for arrival: Dictionary in demand[&"arrivals"]: + command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])), + &"guest_type_id": &"traveler", &"gold": 100, + &"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING, + &"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000, + &"path_handle": {&"target_cell": Comparison.GOAL}}}) + command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])), + &"guest_type_id": &"traveler", &"gold": 100}) + await advance(120) + +func layer_observations(prefix: String) -> void: + await button(main.front_button) + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") + expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report") + await button(main.back_button) + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") + expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report") + +func writer() -> void: + await prepare() + await layer_observations("writer") + capture("presave") + await button(main.find_child("ManualSaveButton", true, false)) + expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save") + capture("checkpoint") + export_package(String(main.manual_save_path), "formal") + var formal := inspect_formal() + var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var")) + expect(presave == var_to_bytes(formal), "whole presave formal model") + expect(not operation_results.is_empty(), "save result recorded") + if not operation_results.is_empty(): + verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED") + var members := {} + for member: String in MEMBERS: + members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member)) + json_file("save.json", {"pid": OS.get_process_id(), "members": members}) + +func reader(distinct: bool) -> void: + var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json"))) + expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID") + await load_menu(main.find_child("SaveLoadMenu", true, false)) + await frames() + expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume") + capture("checkpoint") + var formal := inspect_formal() + expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model") + for member: String in MEMBERS: + expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member) + expect(not operation_results.is_empty(), "load result recorded") + if not operation_results.is_empty(): + verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED") + await layer_observations("reader") + +func finish_observations() -> void: + stream.close() diff --git a/tests/fixtures/p23-03/observation.gd.uid b/tests/fixtures/p23-03/observation.gd.uid new file mode 100644 index 0000000..d2b92c8 --- /dev/null +++ b/tests/fixtures/p23-03/observation.gd.uid @@ -0,0 +1 @@ +uid://dmoi8je5hse38 diff --git a/tests/fixtures/p23-03/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd new file mode 100644 index 0000000..4bb3cf8 --- /dev/null +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -0,0 +1,266 @@ +extends "res://tests/fixtures/p23-03/observation.gd" +## Passive endpoint implementation. All gameplay/input is performed by Root CUA. +## Shared parent contains the handler endpoints, but every such endpoint below +## is overridden; this class never emits UI signals or invokes product handlers. +var receipts: Array = [] +var terminated := false +var terminal_record: Dictionary = {} +var terminal_cause_recorded := false +var reveal_rows: Array = [] +var all_requests: Array = [] +var input_observer: Node +var last_dispatch: Dictionary = {} +var observed_categories: Dictionary = {} + +func observe_progress() -> void: + if role != "writer" or terminated: return + var wanted: Dictionary = {} + var report: Dictionary = main.world_preview.get_life_observation_report() + for room: Dictionary in report.get(&"rooms", {}).values(): + if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true + for badge: Dictionary in report.get(&"landing_badges", []): + if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true + for cabin: Dictionary in report.get(&"cabins", []): + if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true + var elevators: Dictionary = runtime.elevators.get_report() + for shaft: Dictionary in elevators.get(&"shafts", []): + if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true + for trip: Dictionary in elevators.get(&"trips", []): + if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true + for member: StaffRuntime in runtime.staff_service.get_staff_members(): + if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true + if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true + for category: String in wanted: + if observed_categories.has(category): continue + observed_categories[category] = true + await layer_observations("actual-" + category) + if category == "EV-passengers": await motion_observations() + +func motion_observations() -> void: + await button(main.front_button) + await button(main.pause_button) + expect(not clock.paused, "ordinary resume for motion observation") + var minute: int = clock.absolute_minute + while clock.absolute_minute == minute and not terminated: + await tree.process_frame + await frames() + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") + expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history") + await button(main.pause_button) + expect(clock.paused, "ordinary pause for stable observation") + await frames() + expect(await stream.await_draws(tree, main.world_preview), "real draw history before capture") + expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history") + +class InputObserver extends Node: + var events: Array = [] + var endpoint: RefCounted = null + func _input(event: InputEvent) -> void: + if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion: + var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()} + if event is InputEventMouseButton: + row["button"] = event.button_index + row["position"] = [event.position.x, event.position.y] + row["ctrl"] = event.ctrl_pressed + if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + row["dispatch"] = endpoint.dispatch_position(event.position) + events.append(row) + +func expect(ok: bool, message: String) -> void: + if terminated and ok: + return + if terminated and terminal_cause_recorded: + if not ok: + terminal_record["suppressed_after_terminal"] += 1 + json_file(role + "-terminal.json", terminal_record) + return + if terminated and not ok: + terminal_cause_recorded = true + super.expect(ok, message) + +func terminate(stem: String, reason: String) -> void: + terminated = true + terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial, + "stem": stem, "reason": reason, "suppressed_after_terminal": 0} + json_file(role + "-terminal.json", terminal_record) + expect(false, reason) + tree.quit(1) + +func request(action: Dictionary) -> void: + if terminated: + return + serial += 1 + var stem := "%s-request-%04d" % [role, serial] + var folder := directory.path_join(stem) + DirAccess.make_dir_recursive_absolute(folder) + var window: Window = main.get_window() + var view: WorldPreview = main.world_preview + expect(await stream.await_draws(tree, view), "real draw history before capture") + action["report"] = Codec.new().typed(view.get_life_observation_report()) + action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) + action["local_to_viewport"] = affine(view.get_global_transform_with_canvas()) + action["local_to_screen"] = affine(view.get_screen_transform()) + action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view)) + action["requested_ticks_usec"] = Time.get_ticks_usec() + action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0) + action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]} + expect(stream.capture(stem, runtime, view), "request state/report") + action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role, + "state": "waiting_for_root_cua", "window_title": window.title, + "window_position": [window.position.x, window.position.y], + "window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen), + "content_scale_factor": window.content_scale_factor, "setup": "diagnostic"}) + json_file(stem + "/request.json", action) + var path := folder.path_join("ack.json") + var first_event: int = input_observer.events.size() + var deadline := Time.get_ticks_msec() + 300000 + print("P23_ROOT_CUA_WAIT " + folder.path_join("request.json")) + while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline: + await tree.process_frame + if not FileAccess.file_exists(path): + terminate(stem, "Root CUA acknowledgement timeout") + return + var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) + if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")): + terminate(stem, "Root CUA acknowledgement identity/hash") + return + await frames() + for event: Dictionary in input_observer.events.slice(first_event): + if event.has("dispatch"): + last_dispatch = event["dispatch"] + json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial, + "ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")), + "ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0), + "observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch", + "events": input_observer.events.slice(first_event), "status": main.build_status_label.text}) + receipts.append("os-pair/" + layout + "/" + stem + "/ack.json") + all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json") + +func annotate(row: Dictionary) -> void: + var view: WorldPreview = main.world_preview + row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view)) + row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse()) + row["reveal"] = reveal_rows.duplicate(true) + reveal_rows.clear() + row["input_method"] = "native_os" + row["requests"] = receipts.duplicate() + receipts.clear() + +# Rectangles describe existing draw commands; no second renderer is invoked. +func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary: + var rows: Array = [] + for id: Variant in report.get(&"rooms", {}): + var room: Dictionary = report[&"rooms"][id] + if room.has(&"rect"): + rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"}) + for index: int in range(report.get(&"landing_badges", []).size()): + var badge: Dictionary = report[&"landing_badges"][index] + rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"])) + for cabin: Dictionary in report.get(&"cabins", []): + rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"}) + for actor: Dictionary in cabin[&"passengers"]: + rows.append(actor_rectangle(actor)) + for actor: Dictionary in report.get(&"actor_drawings", []): + rows.append(actor_rectangle(actor)) + if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))): + var rest := actor_rectangle(actor) + rest["category"] = "rest" + rows.append(rest) + for index: int in range(report.get(&"actor_aggregations", []).size()): + var badge: Dictionary = report[&"actor_aggregations"][index] + rows.append(badge_rectangle("aggregation", str(index), badge[&"at"], badge[&"text"])) + for index: int in range(report.get(&"badges", []).size()): + var badge: Dictionary = report[&"badges"][index] + rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"])) + for entry: Dictionary in report.get(&"traffic", []): + var traffic: Dictionary = entry[&"traffic"] + if traffic[&"current_density"] > 0 or traffic[&"temporary_block"]: + var cell: Vector3i = entry[&"cell"] + rows.append({"category": "traffic", "id": str(cell.x) + "," + str(cell.y) + "," + str(cell.z), + "rect": entry[&"rect"], "style_source": "scenes/main/world_preview.gd"}) + var transform := view.get_global_transform_with_canvas() + for row: Dictionary in rows: + row["viewport_rect"] = transform * (row["rect"] as Rect2) + row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2) + var source: String = FileAccess.get_file_as_string("res://" + row["style_source"]) + var regex := RegEx.new() + regex.compile('Color\\("([0-9a-fA-F]{6})"') + var colors: Array = [] + for found: RegExMatch in regex.search_all(source): + if not colors.has(found.get_string(1)): colors.append(found.get_string(1)) + row["style_colors"] = colors + return {"rows": rows} + +func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary: + var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6 + return {"category": category, "id": id, "rect": Rect2(at, Vector2(text.length() * 10 + 6, 14)), + "text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"} + +func actor_rectangle(actor: Dictionary) -> Dictionary: + return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"], + "rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"} + +func point(node: Control) -> Dictionary: + var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5) + var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5) + return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(), + "viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())} + +func setup() -> void: + await super.setup() + input_method = "native_os" + expect(DisplayServer.get_name() != "headless", "real OS display") + tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id()) + input_observer = InputObserver.new() + # No handler dispatch: this observer only records incoming OS events. + tree.root.add_child(input_observer) + json_file(role + "-profile.json", {"pid": OS.get_process_id(), + "user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(), + "display": DisplayServer.get_name()}) + +func button(node: BaseButton) -> void: + expect(node != null and not node.disabled, "reachable ordinary button") + if node == null or node.disabled: return + await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))}) + +func load_menu(menu: MenuButton) -> void: + var action := point(menu) + action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."}) + await request(action) + +func screenshot(label: String) -> void: + await frames() + await request({"action": "screenshot", "id": label, "layout": layout}) + +func layer_observations(prefix: String) -> void: + if role == "writer" and prefix == "writer": await screenshot("calibration") + await button(main.front_button) + await screenshot(prefix + "-FRONT") + await button(main.back_button) + await screenshot(prefix + "-BACK") + if role == "writer" and prefix == "writer": await ordinary_camera() + +func ordinary_camera() -> void: + var view: WorldPreview = main.world_preview + var at := point(view) + expect(not view.pan_mode, "ordinary zoom starts with move mode off") + var zoom: float = view.zoom_level + await request({"action": "wheel", "point": at, "direction": "down", + "ctrl": false, "move_mode": false}) + expect(view.zoom_level != zoom, "ordinary wheel changes zoom") + await screenshot("ordinary-wheel-zoom") + await button(main.pan_button) + await screenshot("pan-mode-button") + expect(view.pan_mode, "PanModeButton enables move mode") + zoom = view.zoom_level + var pan: Vector2 = view.pan_offset + await request({"action": "wheel", "point": point(view), "direction": "up", + "ctrl": false, "move_mode": true}) + expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans") + await screenshot("move-mode-wheel-pan") + +func finish_observations() -> void: + json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()}) + if is_instance_valid(input_observer): input_observer.queue_free() + super.finish_observations() + diff --git a/tests/fixtures/p23-03/passive_observation.gd.uid b/tests/fixtures/p23-03/passive_observation.gd.uid new file mode 100644 index 0000000..5a138e4 --- /dev/null +++ b/tests/fixtures/p23-03/passive_observation.gd.uid @@ -0,0 +1 @@ +uid://d3oqtdp6dodu1 diff --git a/tests/fixtures/p23-03/passive_os_recorder.gd b/tests/fixtures/p23-03/passive_os_recorder.gd new file mode 100644 index 0000000..e2b6544 --- /dev/null +++ b/tests/fixtures/p23-03/passive_os_recorder.gd @@ -0,0 +1,8 @@ +extends SceneTree +const Passive := preload("res://tests/fixtures/p23-03/passive_observation.gd") +func _initialize() -> void: + call_deferred("run") +func run() -> void: + var result: Dictionary = await Passive.new().run(self, OS.get_environment("P23_OBSERVATION_ROOT"), OS.get_environment("P23_OBSERVATION_ROLE")) + print("P23 passive OS observation: %d checks, %d failures" % [result.checks, result.failures]) + quit(0 if result.failures == 0 else 1) diff --git a/tests/fixtures/p23-03/passive_os_recorder.gd.uid b/tests/fixtures/p23-03/passive_os_recorder.gd.uid new file mode 100644 index 0000000..50f6f59 --- /dev/null +++ b/tests/fixtures/p23-03/passive_os_recorder.gd.uid @@ -0,0 +1 @@ +uid://mn33trympqu6 diff --git a/tests/fixtures/p23-03/queued_elevator.json b/tests/fixtures/p23-03/queued_elevator.json new file mode 100644 index 0000000..28d1d48 --- /dev/null +++ b/tests/fixtures/p23-03/queued_elevator.json @@ -0,0 +1,27 @@ +{ + "provenance": "Distilled layout-A frame-030 state ac3ed64a6e6162918e4d44c45b2f78d09e50d473b607f3469350a06074a7efb3; retain first real queued trip, no cabin seat. Unit fixture only.", + "saved": { + "version": 1, + "current_minute": 691, + "request_sequence": 8, + "shafts": [{ + "config": { + "shaft_id": "transport.shaft.0.0.0.2", + "module_ids": ["facility_00000003"], + "stops": [[0, 0, 0], [0, 1, 0]], + "width": 2, "capacity": 4, "travel_minutes_per_floor": 10, "access_id": "" + }, + "operational": true, "operator_open": true, "powered": true, + "current_floor": 1, "target_floor": 1, "boarding_floor": 0, + "phase": "PICKUP", "remaining_minutes": 9, "phase_total_minutes": 10, + "phase_started_at": 690, "phase_origin_floor": 1, "phase_passenger_count": 0, + "boarded": [] + }], + "trips": [{ + "actor_id": "guest:p23-000", "request_id": "transport.trip.000000000002", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [0, 0, 0], "to": [0, 1, 0], "enqueued_at": 674, "state": "QUEUED" + }] + } +} diff --git a/tests/fixtures/p23-03/r13_actor_overlap.json b/tests/fixtures/p23-03/r13_actor_overlap.json new file mode 100644 index 0000000..213d793 --- /dev/null +++ b/tests/fixtures/p23-03/r13_actor_overlap.json @@ -0,0 +1,15 @@ +{ + "purpose": "Historical negative only; not fresh acceptance evidence", + "source_request": "os-pair/A/reader-request-0003/request.json", + "source_request_sha256": "7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c", + "manifest_sha256": "53aa8e2f12e5d0a089529457fcd55ee1ba0600e8dffff2225bb8b057d007fe1a", + "independent_judgment": [11, false, false, false], + "source_rows": [11, 29], + "report_inputs": { + "actor_drawings": [ + {"key": "guest/guest-auto-00000041", "at": [651.0, 517.0], "scale": 3.0, "mode": "wait", "frame": 12, "flip": false}, + {"key": "staff/staff_gina", "at": [651.0, 517.0], "scale": 3.0, "mode": "idle", "frame": 3, "flip": false} + ], + "cabins": [] + } +} diff --git a/tests/fixtures/p23-03/r13_service_region_gap.json b/tests/fixtures/p23-03/r13_service_region_gap.json new file mode 100644 index 0000000..5ffdcbe --- /dev/null +++ b/tests/fixtures/p23-03/r13_service_region_gap.json @@ -0,0 +1,17 @@ +{ + "purpose": "Historical diagnostic input for placement-rule clarification; not fresh acceptance evidence", + "source_request": "os-pair/A/reader-request-0003/request.json", + "source_request_sha256": "7dc3ee0bcc428e9d103726ee2448c767f1dd4107f9490833dc860c0b5140068c", + "guests": [ + {"id": "guest-auto-00000044", "state": "RECEIVING_SERVICE", "cell": [5, 0, 0], "target": "facility_00000003", "service": "service_00000009", "queue": ""}, + {"id": "guest-auto-00000019", "state": "RECEIVING_SERVICE", "cell": [-6, 2, 0], "target": "facility_00000007", "service": "service_00000002", "queue": ""} + ], + "room_rects": { + "facility_00000003": [1027.0, 388.0, 256.0, 192.0], + "facility_00000007": [131.0, 292.0, 128.0, 96.0] + }, + "traffic": [ + {"cell": [5, 0, 0], "traffic": {"base_capacity": 4, "current_density": 6, "dynamic_cost": 0, "temporary_block": false}, "rect": [965.0, 486.0, 60.0, 92.0]}, + {"cell": [-6, 2, 0], "traffic": {"base_capacity": 4, "current_density": 3, "dynamic_cost": 0, "temporary_block": false}, "rect": [261.0, 294.0, 60.0, 92.0]} + ] +} diff --git a/tests/fixtures/p23-03/recorded-badge-negative.json.gz b/tests/fixtures/p23-03/recorded-badge-negative.json.gz new file mode 100644 index 0000000..5107266 Binary files /dev/null and b/tests/fixtures/p23-03/recorded-badge-negative.json.gz differ diff --git a/tests/fixtures/p23-03/repair28_frame180_projection.json b/tests/fixtures/p23-03/repair28_frame180_projection.json new file mode 100644 index 0000000..f508b8d --- /dev/null +++ b/tests/fixtures/p23-03/repair28_frame180_projection.json @@ -0,0 +1,2011 @@ +{ + "description": "Unmodified relevant decoded fields from actual frame-180; tests apply COUNTERFACTUAL density=1 in memory. Not engine acceptance evidence or raw receipts.", + "provenance": { + "candidate": "19fc3f50c7ef66223a86b438d4327c5b1b308ed1", + "capture": { + "acceptance": "pending", + "input_method": "diagnostic", + "label": "frame-180", + "motion": "38fa0ea73899e16ded98c14d06c13e4630db2a74a3b4547e0b6d6b2e95dda12f", + "pid": 17350, + "report": "eeabe1205307cf7a460dba8b320c3e284c41223b16ee6887fa31e2018414e8eb", + "rest_audit": "74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2", + "state": "182f098f5c0072bf5a83572930a6a82a8512cbd1da0a5c386a05cdc76ec0cb33", + "state_after": "182f098f5c0072bf5a83572930a6a82a8512cbd1da0a5c386a05cdc76ec0cb33", + "ticks_usec": 6391490 + }, + "files": [ + { + "path": "objects/182f098f5c0072bf5a83572930a6a82a8512cbd1da0a5c386a05cdc76ec0cb33.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-impact-19fc3f5-r01/layout-A/objects/182f098f5c0072bf5a83572930a6a82a8512cbd1da0a5c386a05cdc76ec0cb33.json", + "bytes": 1036025, + "sha256": "182f098f5c0072bf5a83572930a6a82a8512cbd1da0a5c386a05cdc76ec0cb33" + }, + { + "path": "objects/38fa0ea73899e16ded98c14d06c13e4630db2a74a3b4547e0b6d6b2e95dda12f.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-impact-19fc3f5-r01/layout-A/objects/38fa0ea73899e16ded98c14d06c13e4630db2a74a3b4547e0b6d6b2e95dda12f.json", + "bytes": 66789, + "sha256": "38fa0ea73899e16ded98c14d06c13e4630db2a74a3b4547e0b6d6b2e95dda12f" + }, + { + "path": "objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-impact-19fc3f5-r01/layout-A/objects/74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2.json", + "bytes": 286, + "sha256": "74e6f40cdfa031587896827cdaaa8fab67d953dbb7aab42a83ca1c6c993825e2" + }, + { + "path": "objects/eeabe1205307cf7a460dba8b320c3e284c41223b16ee6887fa31e2018414e8eb.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-impact-19fc3f5-r01/layout-A/objects/eeabe1205307cf7a460dba8b320c3e284c41223b16ee6887fa31e2018414e8eb.json", + "bytes": 106546, + "sha256": "eeabe1205307cf7a460dba8b320c3e284c41223b16ee6887fa31e2018414e8eb" + }, + { + "path": "READINESS_19FC3F5_ACTUAL.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/p23-03/READINESS_19FC3F5_ACTUAL.json", + "bytes": 9457, + "sha256": "4eaf2e4c82168d701734802c704c9687232376732ce0f9e28766f395f7cc21b0" + }, + { + "path": "ROOT_19FC3F5_BAND_MISMATCH_REPRO.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/p23-03/ROOT_19FC3F5_BAND_MISMATCH_REPRO.json", + "bytes": 1770, + "sha256": "0d311c447b2d82bd604e3023b30178e5b74dc2a5275590e59305aed9df28c7b6" + }, + { + "path": "OPUS_19FC3F5_BAND_SCOPE_DIRECTION.json", + "original": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_19FC3F5_BAND_SCOPE_DIRECTION.json", + "bytes": 4465, + "sha256": "ce2df30ad24085ae8122cc599613ff14a4a00a3306dd185eef17b67566ee0cd1" + } + ], + "purpose": "Immutable actual diagnostic context plus explicitly counterfactual band test; not normal OS/pixel acceptance" + }, + "report": { + "observation_serial": 22, + "overlay_consumed_serial": 22, + "overlay_passes": [ + "traffic", + "actors" + ], + "overlay_child_indices": [ + 0, + 1, + 2 + ], + "actor_drawings": [ + { + "key": "guest/p23-020", + "center": [ + 576.0, + 621.0 + ], + "at": [ + 560.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 5, + "variant": 5, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-021", + "center": [ + 624.0, + 621.0 + ], + "at": [ + 608.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 6, + "variant": 6, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-022", + "center": [ + 672.0, + 621.0 + ], + "at": [ + 656.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 7, + "variant": 7, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-024", + "center": [ + 528.0, + 621.0 + ], + "at": [ + 512.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 9, + "variant": 9, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": true, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-025", + "center": [ + 384.0, + 621.0 + ], + "at": [ + 368.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 10, + "variant": 10, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-026", + "center": [ + 480.0, + 621.0 + ], + "at": [ + 464.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 11, + "variant": 11, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": true, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-027", + "center": [ + 432.0, + 621.0 + ], + "at": [ + 416.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 12, + "variant": 12, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-028", + "center": [ + 336.0, + 621.0 + ], + "at": [ + 320.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 13, + "variant": 13, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-029", + "center": [ + 816.0, + 621.0 + ], + "at": [ + 800.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 14, + "variant": 14, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-030", + "center": [ + 864.0, + 621.0 + ], + "at": [ + 848.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 8, + "variant": 8, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-031", + "center": [ + 768.0, + 621.0 + ], + "at": [ + 752.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 9, + "variant": 9, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-032", + "center": [ + 1056.0, + 621.0 + ], + "at": [ + 1040.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 10, + "variant": 10, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-033", + "center": [ + 912.0, + 621.0 + ], + "at": [ + 896.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 11, + "variant": 11, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": true, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-034", + "center": [ + 960.0, + 621.0 + ], + "at": [ + 944.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 12, + "variant": 12, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-035", + "center": [ + 1008.0, + 621.0 + ], + "at": [ + 992.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 13, + "variant": 13, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-036", + "center": [ + 1104.0, + 621.0 + ], + "at": [ + 1088.0, + 614.0 + ], + "scale": 2.0, + "mode": "wait", + "frame": 14, + "variant": 14, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": false, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + } + ], + "cabins": [ + { + "shaft_id": "transport.shaft.0.0.0.2", + "rect": [ + 708.0, + 541.0, + 72.0, + 57.0 + ], + "pixel": 3.0, + "operational": true, + "doors_open": false, + "passengers": [ + { + "key": "guest/p23-018", + "center": [ + 726.0, + 565.0 + ], + "at": [ + 718.0, + 575.0 + ], + "scale": 1.0, + "mode": "idle", + "frame": 10, + "variant": 10, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": true, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-019", + "center": [ + 750.0, + 565.0 + ], + "at": [ + 742.0, + 575.0 + ], + "scale": 1.0, + "mode": "idle", + "frame": 11, + "variant": 11, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": true, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-017", + "center": [ + 726.0, + 538.0 + ], + "at": [ + 718.0, + 548.0 + ], + "scale": 1.0, + "mode": "idle", + "frame": 9, + "variant": 9, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": true, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + }, + { + "key": "guest/p23-023", + "center": [ + 750.0, + 538.0 + ], + "at": [ + 742.0, + 548.0 + ], + "scale": 1.0, + "mode": "idle", + "frame": 8, + "variant": 8, + "staff": false, + "state": 3, + "receiving": false, + "carrying": false, + "activity": "", + "riding": true, + "flip": false, + "cell": [ + 0, + 0, + 0 + ] + } + ], + "badge_at": [ + 708.0, + 527.0 + ], + "badge": "満員 4/4人" + } + ], + "traffic": [ + { + "cell": [ + -12, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 122.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 127.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -11, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 170.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 175.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -10, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 218.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 223.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -9, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 266.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 271.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -8, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 314.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 319.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -7, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 362.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 367.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -6, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 410.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 415.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -5, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 458.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 463.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -4, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 506.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 511.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -3, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 554.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 559.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -2, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 602.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 607.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + -1, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 650.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 655.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 0, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 27, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 698.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 703.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 1, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 746.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 751.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 2, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 794.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 799.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 3, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 842.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 847.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 4, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 890.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 895.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 5, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 938.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 943.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 6, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 986.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 991.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 7, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1034.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1039.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 8, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1082.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1087.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 9, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1130.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1135.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 10, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1178.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1183.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 11, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1226.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1231.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 12, + 0, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 1274.0, + 587.0, + 44.0, + 68.0 + ], + "band_rect": [ + 1279.28, + 648.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 0, + 1, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 698.0, + 515.0, + 44.0, + 68.0 + ], + "band_rect": [ + 703.28, + 576.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 1, + 1, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 746.0, + 515.0, + 44.0, + 68.0 + ], + "band_rect": [ + 751.28, + 576.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 2, + 1, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 794.0, + 515.0, + 44.0, + 68.0 + ], + "band_rect": [ + 799.28, + 576.5, + 33.44, + 5.0 + ] + }, + { + "cell": [ + 3, + 1, + 0 + ], + "traffic": { + "base_capacity": 4, + "current_density": 0, + "dynamic_cost": 0, + "temporary_block": false + }, + "rect": [ + 842.0, + 515.0, + 44.0, + 68.0 + ], + "band_rect": [ + 847.28, + 576.5, + 33.44, + 5.0 + ] + } + ], + "layer": 0, + "viewport": [ + 1440.0, + 810.0 + ], + "zoom": 1.0, + "pan": [ + 0.0, + 0.0 + ], + "elevator_tracks": { + "transport.shaft.0.0.0.2": { + "floor": 0.7, + "from": 0.9, + "at": 0.0 + } + }, + "animation_time": 0.0, + "paused": true + }, + "snapshot": { + "systems": { + "runtime": { + "world": { + "bounds": { + "min_x": -12, + "max_x": 12, + "min_y": 0, + "max_y": 6 + } + }, + "elevators": { + "version": 1, + "current_minute": 841, + "request_sequence": 48, + "shafts": [ + { + "config": { + "shaft_id": "transport.shaft.0.0.0.2", + "module_ids": [ + "facility_00000003" + ], + "stops": [ + [ + 0, + 0, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "width": 2, + "capacity": 4, + "travel_minutes_per_floor": 10, + "access_id": "" + }, + "operational": true, + "operator_open": true, + "powered": true, + "current_floor": 0, + "target_floor": 1, + "boarding_floor": 0, + "phase": "TRAVEL", + "remaining_minutes": 3, + "phase_total_minutes": 10, + "phase_started_at": 834, + "phase_origin_floor": 0, + "phase_passenger_count": 4, + "boarded": [ + "guest:p23-018", + "guest:p23-019", + "guest:p23-017", + "guest:p23-023" + ] + } + ], + "trips": [ + { + "actor_id": "guest:p23-017", + "request_id": "transport.trip.000000000020", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 728, + "state": "BOARDED" + }, + { + "actor_id": "guest:p23-018", + "request_id": "transport.trip.000000000018", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 727, + "state": "BOARDED" + }, + { + "actor_id": "guest:p23-019", + "request_id": "transport.trip.000000000019", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 727, + "state": "BOARDED" + }, + { + "actor_id": "guest:p23-020", + "request_id": "transport.trip.000000000024", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 743, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-021", + "request_id": "transport.trip.000000000023", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 742, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-022", + "request_id": "transport.trip.000000000022", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 741, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-023", + "request_id": "transport.trip.000000000021", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 740, + "state": "BOARDED" + }, + { + "actor_id": "guest:p23-024", + "request_id": "transport.trip.000000000025", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 756, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-025", + "request_id": "transport.trip.000000000028", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 757, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-026", + "request_id": "transport.trip.000000000026", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 756, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-027", + "request_id": "transport.trip.000000000027", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 756, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-028", + "request_id": "transport.trip.000000000029", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 771, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-029", + "request_id": "transport.trip.000000000031", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 772, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-030", + "request_id": "transport.trip.000000000032", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 772, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-031", + "request_id": "transport.trip.000000000030", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 771, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-032", + "request_id": "transport.trip.000000000036", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 787, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-033", + "request_id": "transport.trip.000000000033", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 786, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-034", + "request_id": "transport.trip.000000000034", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 786, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-035", + "request_id": "transport.trip.000000000035", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 786, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-036", + "request_id": "transport.trip.000000000037", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 802, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-037", + "request_id": "transport.trip.000000000038", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 802, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-038", + "request_id": "transport.trip.000000000039", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 802, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-039", + "request_id": "transport.trip.000000000040", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 802, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-040", + "request_id": "transport.trip.000000000042", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 816, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-041", + "request_id": "transport.trip.000000000044", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 818, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-042", + "request_id": "transport.trip.000000000041", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 815, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-043", + "request_id": "transport.trip.000000000043", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 817, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-044", + "request_id": "transport.trip.000000000046", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 832, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-045", + "request_id": "transport.trip.000000000047", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 832, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-046", + "request_id": "transport.trip.000000000045", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 829, + "state": "QUEUED" + }, + { + "actor_id": "guest:p23-047", + "request_id": "transport.trip.000000000048", + "shaft_id": "transport.shaft.0.0.0.2", + "connector_id": "transport.shaft.0.0.0.2.link.0.1", + "from": [ + 0, + 0, + 0 + ], + "to": [ + 0, + 1, + 0 + ], + "enqueued_at": 832, + "state": "QUEUED" + } + ] + }, + "staff": { + "tasks": [] + } + } + } + } +} diff --git a/tests/fixtures/p23-03/repair28_r15_negative.json b/tests/fixtures/p23-03/repair28_r15_negative.json new file mode 100644 index 0000000..0de5c3b --- /dev/null +++ b/tests/fixtures/p23-03/repair28_r15_negative.json @@ -0,0 +1,14 @@ +{ + "applicability": "Historical negative only. Distilled local-coordinate fields; never S/T execution proof.", + "source_commit": "9a26f9ab8c6691ba3cbe32ac4bc4525f4a9c7106", + "manifest_sha256": "a25e15f00d09acb16b05e180a6263c3771c84449bd6e0c99b6968fd739727bd2", + "request_sha256": "7099ca03da3cef5819e7965f6fdea8777b9995a9e12806e870e844ec2b02b9a5", + "request_relative_to_R15": "os-pair/A/reader-request-0003/request.json", + "actual_negative_rows": [6, 17], + "cabin_rect": [659, 463, 96, 76], + "passenger_count": 0, + "actor": {"key": "guest/os-p23-024", "center": [739, 532], "at": [715, 517], "scale": 3, "mode": "wait", "frame": 6, "flip": false}, + "traffic": {"cell": [0, 1, 0], "rect": [645, 390, 60, 92], "band_rect": [652.2, 473.5, 45.6, 5], "traffic": {"base_capacity": 4, "current_density": 16, "dynamic_cost": 0, "temporary_block": false}}, + "queue_badge": {"at": [563, 468], "text": "待ち27人(他18人)"}, + "note": "Raw R15 maps/receipts remain unchanged. New geometry tests calculate a counterfactual projection in memory only." +} diff --git a/tests/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd new file mode 100644 index 0000000..6cf5c50 --- /dev/null +++ b/tests/ui/test_p23_life_observation.gd @@ -0,0 +1,263 @@ +extends SceneTree +## Component and read-only rendering checks; never OS or same-demand proof. +const Presentation := preload("res://scripts/ui/room_presentation.gd") +const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd") +var checks := 0 +var failures := 0 + +class QueueWorld extends RefCounted: + var revision := 0 + func get_bounds() -> Dictionary: + return {&"min_x": -1, &"max_x": 2, &"min_y": 0, &"max_y": 1} + func is_walkable(cell: Vector3i) -> bool: + return cell.y >= 0 and cell.y <= 1 and cell.x >= -1 and cell.x <= 2 + func get_occupant(_cell: Vector3i) -> StringName: + return &"" + +class QueueLift extends RefCounted: + var layer := 0 + var visual_floor := 0.3 + func get_report() -> Dictionary: + var trips: Array = [] + for i: int in 3: + trips.append({&"state": PathService.ELEVATOR_REQUEST_QUEUED, &"from": Vector3i(0,0,layer), + &"actor_id": "guest:q%d" % i, &"enqueued_at": i}) + return {&"shafts": [{&"shaft_id": &"wide", &"stops": [Vector3i(0,0,layer), Vector3i(0,1,layer)], + &"width": 2, &"capacity": 4, &"boarded": [], &"visual_floor": visual_floor, + &"operational": true, &"phase": &"PICKUP"}], &"trips": trips} + +class QueueRuntime extends RefCounted: + var world_service := QueueWorld.new() + var elevators := QueueLift.new() + +func repair28_geometry() -> void: + var view := WorldPreview.new() + view.size = Vector2(408,332) + var fixture := QueueRuntime.new() + view._runtime = fixture + root.add_child(view) + for layer: int in [0,1]: + view.layer_mode = layer as WorldPreview.LayerMode + fixture.elevators.layer = layer + var model: Dictionary = view._observe_queues() + expect(model[&"shaft_footprint"].has(Vector3i(1,0,layer)), "width-2 interior belongs to shaft") + expect(model[&"queue_positions"].size() == 1 and model[&"queue_hidden"].size() == 2, "shaft stops outward lane and retains overflow") + var badge: Dictionary = model[&"landing_badges"][0] + expect(badge[&"members"].size() == badge[&"visible_count"] + badge[&"overflow"], "no queue member lost") + model.merge({&"boarded": {}, &"elevator_tracks": {}, &"paused": true}) + var cabins: Array[Dictionary] = view._observe_elevators(model) + expect(cabins.size() == 1, "active-layer cabin remains visible") + for at: Vector2 in model[&"queue_positions"].values(): + for cell: Vector3i in model[&"shaft_footprint"]: + expect(not view.get_cell_rect(cell).has_point(at), "queue anchor outside full footprint") + expect(not cabins[0][&"rect"].has_point(at), "queue anchor outside empty cabin") + expect(cabins[0][&"passenger_count"] == 0 and cabins[0][&"passengers"].is_empty(), "empty cabin has no passenger") + expect(cabins[0][&"operational"] and not cabins[0][&"doors_open"] and cabins[0][&"phase"] == &"PICKUP", "typed moving cabin state") + for floor: float in [0.0, 0.8]: + fixture.elevators.visual_floor = floor + var pose_model: Dictionary = view._observe_queues() + var cell := Vector3i(0, 0 if floor == 0 else 1, layer) + var key := "guest/landing" + var person := {&"id": &"landing", &"cell": cell, &"service": &"", &"target": &""} + pose_model.merge({&"boarded": {}, &"elevator_tracks": {}, &"paused": true, + &"actor_tracks": {key: {&"cell": cell, &"from": cell, &"at": 0.0, &"direction": 1.0}}, + &"rooms": {}, &"guests": [person], &"staff": [], &"carried_patients": {}, + &"resting_staff": {}, &"treated_patients": {}}) + pose_model[&"cabins"] = view._observe_elevators(pose_model) + var row: Dictionary = view._actor_drawing(view.cell_to_canvas(cell), key, false, + GuestRuntime.State.MOVING, false, false, &"", false, pose_model) + var region: Dictionary = view._life_actor_region(row, person, pose_model) + expect(region.has(&"center") and not region[&"transit"], "same-floor and fractional cabin use landing region") + expect(not (region[&"bounds"] as Rect2).intersects(pose_model[&"cabins"][0][&"rect"]), "landing bounds outside cabin") + pose_model[&"actor_drawings"] = [row] + view._fit_life_actors(pose_model) + expect(pose_model[&"actor_drawings"].size() == 1 and pose_model[&"actor_aggregations"].is_empty(), "landing actor retained") + expect(not view._life_core(row).intersects(pose_model[&"cabins"][0][&"rect"]), "fitted non-passenger core outside cabin") + expect(row[&"cell"] == cell and not row[&"riding"] and row[&"state"] == GuestRuntime.State.MOVING, "landing placement preserves actual state") + fixture.elevators.visual_floor = 0.3 + for operational: bool in [true,false]: + for phase: StringName in [&"IDLE", &"BOARDING", &"PICKUP"]: + var shaft: Dictionary = fixture.elevators.get_report()[&"shafts"][0] + shaft[&"operational"] = operational + shaft[&"phase"] = phase + var text := Presentation.elevator_label(shaft) + expect(text.contains("0/4人") and text.contains(String(phase)), "count and phase visible") + expect(text.begins_with("運転" if operational else "停止"), "operation visible beyond lamp") + expect(text.contains(" 開 " if operational and phase != &"PICKUP" else " 閉 "), "doors visible beyond lamp") + var obstacles: Array[Rect2] = [Rect2(20,0,20,90), Rect2(45,80,20,14)] + var segments := view._traffic_segments(Rect2(10,85,60,5), Rect2(0,0,80,96), obstacles) + var area := 0.0 + for segment: Rect2 in segments: + area += segment.get_area() + for obstacle: Rect2 in obstacles: + expect(not segment.intersects(obstacle), "traffic never crosses cabin or badge") + expect(is_equal_approx(area, 100.0), "traffic preserves all uncovered area") + var covering: Array[Rect2] = [Rect2(0,80,80,16)] + var fallback := view._traffic_segments(Rect2(10,85,60,5), Rect2(0,0,80,96), covering) + expect(fallback.size() == 1 and fallback[0] == Rect2(10,0,60,5), "shaft-column density remains visible") + view.free() + +func _initialize() -> void: + call_deferred("run") + +func repair36_labels() -> void: + var view := WorldPreview.new() + var members: Array[String] = ["guest/a", "guest/b", "guest/c"] + var identities: Array[Dictionary] = [] + for key: String in members: + identities.append({&"key": key, &"id": key.trim_prefix("guest/"), &"state": 0, &"cell": Vector3i.ZERO, &"mode": &"idle"}) + var queue := {&"at": Vector2(40,40), &"text": "待ち3人(他3人)", &"cell": Vector3i.ZERO, &"members": members.duplicate(), &"visible_count": 0, &"overflow": members.size()} + var aggregate := {&"at": queue[&"at"], &"text": "+3", &"members": identities.duplicate(true), &"drawn_keys": []} + var model := {&"cabins": [], &"badges": [], &"landing_badges": [queue], &"actor_aggregations": [aggregate]} + var blocked := Rect2(100,100,20,20) + model[&"cabins"] = [{&"rect": blocked, &"badge_at": Vector2(200,200), &"badge": "EV", &"badge_rect": view._readable_badge_rect(Vector2(200,200), "EV")}] + model[&"shaft_footprint"] = [] + var region: Dictionary = view._life_landing_region({&"bounds": blocked, &"transit": false}, Vector3i.ZERO, model) + expect(region.get(&"no_lane", false) and region[&"badge_at"] == queue[&"at"], "lane-less stranded actor uses hidden queue anchor") + view._resolve_observation_labels(model) + expect(not view._readable_badge_rect(queue[&"at"], queue[&"text"]).intersection(view._readable_badge_rect(aggregate[&"at"], aggregate[&"text"])).has_area(), "hidden queue and stranded aggregate labels do not overlap") + expect(queue[&"text"] == "待ち3人(他3人)" and aggregate[&"text"] == "+3", "complete counts remain readable") + expect(queue[&"members"] == members and aggregate[&"members"] == identities and aggregate[&"drawn_keys"].is_empty(), "label placement preserves member identities and visibility") + expect(queue[&"at"] == queue[&"anchor"] and aggregate[&"anchor"] == queue[&"anchor"], "uncontested queue keeps anchor and stranded anchor remains recorded") + var solitary := {&"at": Vector2(400,400), &"text": "+1"} + model[&"actor_aggregations"] = [solitary] + view._resolve_observation_labels(model) + expect(solitary[&"at"] == solitary[&"anchor"] and solitary[&"displaced_by"].is_empty(), "uncontested label stays at anchor") + view.free() + +func expect(value: bool, message: String) -> void: + checks += 1 + if not value: + failures += 1 + push_error("P23 life observation: " + message) + +func run() -> void: + var layout := OS.get_environment("P23_LAYOUT") + var output_directory := OS.get_environment("P23_OUTPUT") + if layout in ["A", "B"] and not output_directory.is_empty(): + var comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd").new() + expect(await comparison.run(self, output_directory, layout), "native layout " + layout) + print("P23 life observation: %d checks, %d failures" % [checks, failures]) + quit(0 if failures == 0 else 1) + return + var clock: Variant = root.get_node("GameClock") + clock.set_paused(true) + repair28_geometry() + repair36_labels() + # Isolated real typed components test state labels, without claiming gameplay. + var labels: Dictionary = {} + for state: int in FacilityInstance.State.values(): + var room := FacilityInstance.new().configure(&"component", &"reception") + room.state = state as FacilityInstance.State + var description := Presentation.describe(room) + expect(description[&"state_name"] == FacilityInstance.State.keys()[state], "enum name preserved") + var text := Presentation.business_label(description) + expect(not text.is_empty() and not labels.has(text), "distinct visible business reason") + labels[text] = true + for reason: StringName in [&"RAIDED", &"DESTROYED"]: + var room := FacilityInstance.new().configure(&"component", &"reception", {&"suspension_reason": reason}) + var description := Presentation.describe(room) + expect(description[&"operation"] == FacilityInstance.OperationState.SUSPENDED, "actual suspension operation") + var text := Presentation.business_label(description) + expect(not labels.has(text), "raid and damage have separate visible labels") + labels[text] = true + var room := FacilityInstance.new().configure(&"component", &"reception", {&"customer_capacity": 3}) + var service := ServiceInstance.new() + service.guest_ids.assign([&"b", &"a"]) + room.active_services[&"service"] = service + room.queue_entries.assign([&"queue-only"]) + var description := Presentation.describe(room) + expect(description[&"people"] == [&"a", &"b"] and description[&"count"] == 2, "queue entries are not room occupants") + description[&"people"].clear() + expect(service.guest_ids.size() == 2, "presentation owns its arrays") + var runtime: Variant = root.get_node("SimulationRuntime") + var view := WorldPreview.new() + view.size = Vector2(1440, 810) + view.set_runtime(runtime) + root.add_child(view) + # R13's four same-cell MOVING guests. Component fixture only, no Runtime edit. + var crowd: Array = [] + var offsets := [0, 16, -16, 8] + for i: int in 4: + crowd.append({&"row": {&"key": "guest/os-p23-%03d" % (12+i), + &"center": Vector2(108+offsets[i], 118-4*(i%2)), + &"at": Vector2(84+offsets[i], 103-4*(i%2)), &"scale": 3.0, + &"mode": &"idle", &"frame": 0, &"flip": false, + &"state": GuestRuntime.State.MOVING, &"cell": Vector3i(1,1,0)}, + &"bounds": Rect2(80,74,56,88), &"transit": false}) + crowd.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return a[&"row"][&"center"].x < b[&"row"][&"center"].x) + # _life_pack uses the view's cell dimensions for the unchanged feet formula. + # The x-capacity assertion is independent of the current default world size. + expect(view._life_pack(crowd, 3).is_empty(), "four cores cannot fit 72px in 56px") + var saved_size := view.size + var saved_zoom := view.zoom_level + var dim: Vector2 = view._cell_dimensions() + view.zoom_level *= 64.0/dim.x + var fitted: Array = view._life_pack(crowd, 2) + expect(fitted.size() == 4, "all four R13 occupants fit at p=2") + for row: Dictionary in fitted: + expect(row[&"scale"] == 2.0 and row[&"cell"] == Vector3i(1,1,0), "fitting preserves cell and uses p=2") + var regroup: Array = crowd.slice(0, 2).duplicate(true) + for i: int in 2: + regroup[i][&"row"][&"center"] = Vector2(108, 118+26*i) + regroup[i][&"bounds"] = Rect2(80,74,20,130) + var regrouped: Array = view._life_fitted_groups(regroup) + expect(regrouped.size() == 2, "scale change splits vertical component") + for group: Array in regrouped: + expect(group.size() == 1 and group[0][&"row"][&"scale"] == 2.0, "regroup avoids stale p=1 fit") + var saturated: Array = [] + for i: int in 10: + var member: Dictionary = crowd[0].duplicate(true) + member[&"row"][&"key"] = "guest/saturation-%02d" % i + member[&"row"][&"center"] = Vector2(108,118) + saturated.append(member) + var saturated_groups: Array = view._life_fitted_groups(saturated) + expect(saturated_groups.size() == 1, "saturation keeps one vertical component") + expect(view._life_pack(saturated_groups[0], 1).is_empty(), "ten cores cannot fit 60px in 56px") + expect(view._life_pack(saturated_groups[0].slice(0,9), 1).size() == 9, "nine cores fit without losing identities") + var aggregate_model := {&"guests": [], &"staff": [], &"actor_drawings": [], &"cabins": []} + var aggregate_rows: Array = [] + for member: Dictionary in saturated: + var row: Dictionary = member[&"row"].duplicate(true) + row[&"riding"] = true + aggregate_rows.append(row) + aggregate_model[&"guests"].append({&"id": String(row[&"key"]).trim_prefix("guest/"), &"cell": Vector3i(1,1,0)}) + aggregate_model[&"cabins"].append({&"rect": Rect2(76,70,64,96), &"passengers": aggregate_rows}) + view._fit_life_actors(aggregate_model) + expect(aggregate_model[&"cabins"][0][&"passengers"].size() == 9, "aggregation draws all nine fitting members") + var aggregate: Dictionary = aggregate_model[&"actor_aggregations"][0] + expect(aggregate[&"members"].size() == 10 and aggregate[&"drawn_keys"].size() == 9, "aggregation retains every identity") + expect(aggregate[&"text"] == "+1" and aggregate[&"at"] == Vector2(80,74), "actual aggregation badge position and text") + view.zoom_level = saved_zoom + view.size = saved_size + var before: PackedByteArray = var_to_bytes(runtime.create_snapshot()) + var tracks: PackedByteArray = var_to_bytes([view._actor_tracks, view._elevator_tracks]) + var model := view._build_life_observation() + expect(var_to_bytes(runtime.create_snapshot()) == before, "builder does not mutate Runtime") + expect(var_to_bytes([view._actor_tracks, view._elevator_tracks]) == tracks, "builder does not advance visual tracks") + expect(model.has(&"rooms") and model.has(&"landing_badges") and model.has(&"elevators") and model.has(&"staff") and model.has(&"goods") and model.has(&"badges"), "all six category fields exist") + expect(model[&"traffic"].size() > 0 and not view.show_grid, "normal view observes real walkable traffic") + await process_frame + await process_frame + var report := view.get_life_observation_report() + expect(not report.is_empty(), "actual drawing published an observation") + expect(report.get(&"observation_serial", -1) == report.get(&"overlay_consumed_serial", -2), "LifeOverlay consumed this observation") + expect(report.get(&"overlay_passes", []) == [&"traffic", &"cabins", &"actors"], "traffic drawn before cabins and actors") + var indices: Array = report.get(&"overlay_child_indices", []) + expect(indices.size() == 3 and indices[0] < indices[1] and indices[1] < indices[2], "LifeOverlay above inactive art and below lens") + # The getter exposes the last actual draw and must not create another model. + var original: PackedByteArray = var_to_bytes(report) + if report.has(&"traffic"): + report[&"traffic"].clear() + expect(var_to_bytes(view.get_life_observation_report()) == original, "nested report arrays are isolated") + report.clear() + expect(var_to_bytes(view.get_life_observation_report()) == original, "public report is a deep copy") + var output := OS.get_environment("P23_LIFE_DIAGNOSTIC_ROOT") + if not output.is_empty(): + var stream := Stream.new() + expect(stream.open(output), "fresh sparse diagnostic stream") + expect(stream.capture("initial", runtime, view), "whole typed state and draw report capture") + stream.close() + view.free() + print("P23 life observation: %d checks, %d failures" % [checks, failures]) + quit(0 if failures == 0 else 1) diff --git a/tests/ui/test_p23_life_observation.gd.uid b/tests/ui/test_p23_life_observation.gd.uid new file mode 100644 index 0000000..1dd49ef --- /dev/null +++ b/tests/ui/test_p23_life_observation.gd.uid @@ -0,0 +1 @@ +uid://d23lifeobs03 diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py new file mode 100644 index 0000000..8a62d1e --- /dev/null +++ b/tools/check_p23_life_observation_evidence.py @@ -0,0 +1,2503 @@ +#!/usr/bin/env python3 +"""Partial read-only semantic diagnostics. This version cannot accept P23-03. + +Whole typed inputs are preserved. The existing typed codec is reused verbatim. +Unimplemented visual/OS checks are explicit blockers, never successful checks. +""" +import argparse +import contextlib +import tempfile +import copy +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import re +import sys +import struct +import subprocess +import zlib + +sys.dont_write_bytecode = True +ROOT = Path(__file__).resolve().parent.parent +SPEC = 'docs/spec/acceptance/P23-03.json' +SPEC_SHA = '8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f' +CONDITIONS = ( + 'room_occupancy_and_operation', 'queues_and_overflow', + 'elevator_state_and_passengers', 'staff_state_and_jobs', + 'supplies_and_carried_goods', 'same_demand_route_improvement', + 'normal_os_and_distinct_pid_resume', +) +CATEGORIES = ('room', 'queue', 'EV', 'staff', 'goods', 'business') +CONDITION_CHECKS = dict(zip(CONDITIONS, ('room_users', 'queue_overflow', 'elevator_passengers', + 'staff_jobs', 'carried_goods', 'route_improvement', 'distinct_pid_resume'))) +# Required names, not verdicts. BLOCKERS is computed from actual execution below. +REQUIRED_CHECKS = {condition: {category: {'semantic_' + category, 'pixels_' + category, + CONDITION_CHECKS[condition], 'os_guards', 'resume_recalculation'} + for category in CATEGORIES} for condition in CONDITIONS} + + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + result = importlib.util.module_from_spec(spec) + spec.loader.exec_module(result) + return result + + +codec = module('p23_existing_typed_codec', ROOT / 'tools/check_p08_migration_evidence.py') +def decode(node): + # Keep P08 scalar semantics, adding the renderer's Rect2 and vector keys. + if node['type'] == 'Dictionary': + result = {} + for key, value in node['entries']: + key = decode(key) + if isinstance(key, list): + key = tuple(key) + require(key not in result, 'duplicate typed dictionary key') + result[key] = decode(value) + return result + if node['type'] == 'Array': + return [decode(value) for value in node['items']] + if node['type'] == 'Rect2': + match = re.fullmatch(r'Rect2\(([^()]*)\)', node['text']) + require(match is not None, 'invalid typed Rect2') + values = [float(v.strip()) for v in match[1].split(',')] + require(len(values) == 4 and all(math.isfinite(v) for v in values), 'invalid rectangle') + return values + return codec.decode(node) + + +def require(value, message): + if not value: + raise ValueError(message) + + +def check_setup_budget_source(source): + """Cheap preflight for the fixture's shared spend plan, before OS replay.""" + prepare = source.split('func prepare() -> void:', 1)[1].split('\nfunc ', 1)[0] + categories = { + 'construction': ('budget_builds := builds.duplicate(true)', + 'budget_builds.append([improvement[', + 'for row: Array in budget_builds:', + 'needed[key] += int(definition.construction_cost.get(key, 0))'), + 'signing': ('for assignment: Array in assignments:', + 'runtime.mvp_content_catalog.get_recruitment_entry(assignment[0])', + 'signing_total += signing_cost', 'needed[&"gold"] += signing_total'), + 'purchase': ('SupplyCatalog.get_item(SupplyCatalog.RAW_FOOD)', + 'PurchaseQuoteV2.make(SupplyCatalog.RAW_FOOD, purchase_quantity,', + 'PurchaseQuoteV2.eligible(runtime)', 'purchase_quote[&"effective_total"]', + 'needed[&"gold"] += purchase_total'), + } + budget_end = prepare.index('\n\tvar state:') + for category, expressions in categories.items(): + require(all(expression in prepare[:budget_end] for expression in expressions), + 'setup budget missing spend category: ' + category) + require(prepare.count('for assignment: Array in assignments:') == 2, + 'budget and hire must share assignments') + require('&"quantity": purchase_quantity}' in prepare, + 'budget and purchase must share quantity') + require('deltas[key] = needed[key] - state.get_resource(key)' in prepare, + 'setup budget deltas must cover needed') + return set(categories) + + +def sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +def png_pixels(data): + """Decode noninterlaced 8-bit RGB/RGBA PNG, validating the whole stream.""" + require(data[:8] == b'\x89PNG\r\n\x1a\n', 'PNG signature') + pos, header, payload, ended, idat_closed = 8, None, bytearray(), False, False + while pos < len(data): + require(pos + 12 <= len(data), 'PNG truncated chunk') + size, kind = struct.unpack('>I4s', data[pos:pos + 8]) + require(pos + size + 12 <= len(data), 'PNG truncated payload') + body = data[pos + 8:pos + 8 + size] + crc = struct.unpack('>I', data[pos + 8 + size:pos + 12 + size])[0] + require(zlib.crc32(kind + body) == crc, 'PNG CRC mismatch') + pos += size + 12 + require(header is not None or kind == b'IHDR', 'PNG IHDR must be first') + if kind == b'IHDR': + require(header is None and size == 13, 'PNG duplicate/invalid IHDR') + w, h, depth, color, compression, filtering, interlace = struct.unpack('>IIBBBBB', body) + require(w > 0 and h > 0 and depth == 8 and color in (2, 6) + and (compression, filtering, interlace) == (0, 0, 0), 'PNG unsupported IHDR') + header = (w, h, 3 if color == 2 else 4) + elif kind == b'IDAT': + require(not idat_closed, 'PNG nonconsecutive IDAT') + payload.extend(body) + elif kind == b'IEND': + require(size == 0 and payload and pos == len(data), 'PNG invalid IEND') + ended = True + break + else: + require(kind[0] & 32 or kind == b'PLTE', 'PNG unknown critical chunk') + if payload: + idat_closed = True + require(ended and header is not None, 'PNG missing IEND') + w, h, channels = header + stride = w * channels + expected = h * (stride + 1) + try: + decoder = zlib.decompressobj() + raw = decoder.decompress(bytes(payload), expected + 1) + require(decoder.eof and not decoder.unused_data and not decoder.unconsumed_tail + and len(raw) == expected, 'PNG zlib length/end mismatch') + except zlib.error as exc: + raise ValueError('PNG invalid zlib') from exc + rows, prior = [], bytearray(stride) + for y in range(h): + start = y * (stride + 1) + mode = raw[start] + require(mode in range(5), 'PNG unknown filter') + row = bytearray(raw[start + 1:start + 1 + stride]) + for x in range(stride): + left = row[x - channels] if x >= channels else 0 + up = prior[x] + corner = prior[x - channels] if x >= channels else 0 + if mode == 1: + predictor = left + elif mode == 2: + predictor = up + elif mode == 3: + predictor = (left + up) // 2 + elif mode == 4: + p = left + up - corner + distances = (abs(p - left), abs(p - up), abs(p - corner)) + predictor = (left, up, corner)[distances.index(min(distances))] + else: + predictor = 0 + row[x] = (row[x] + predictor) & 255 + rows.append([tuple(row[x:x + channels]) for x in range(0, stride, channels)]) + prior = row + return w, h, rows + + +def image_kind(data): + if data.startswith(b'\x89PNG\r\n\x1a\n'): + w, h, _ = png_pixels(data) + return 'png', w, h + if data.startswith(b'\xff\xd8'): + helper = module('p23_p08_jpeg', ROOT / 'tools/check_p08_normal_placement_evidence.py') + w, h = helper.jpeg_dimensions(data) + return 'jpeg', w, h + raise ValueError('unknown screenshot byte magic') + + +def inspect_conversion(rec, cua_receipt, converter_sha256=None, resolve_path=Path): + """CUA receipt and converter pin are independent of conversion assertions.""" + raw, png = resolve_path(rec['raw_path']), resolve_path(rec['png_path']) + require(raw.resolve() != png.resolve() and not raw.is_symlink() and not png.is_symlink(), + 'conversion must preserve a separate raw file') + require(rec['raw_sha256'] == sha(raw) == cua_receipt['raw_sha256'] + and rec['tool_call_id'] == cua_receipt['tool_call_id'] + and bool(rec['tool_call_id']), 'CUA raw hash/call mismatch') + require(rec['raw_kind'] == 'jpeg', 'conversion raw kind') + kind, w, h = image_kind(raw.read_bytes()) + require(kind == 'jpeg', 'conversion requires JPEG magic') + require(rec['converter'] == '/usr/bin/sips', 'conversion executable') + pin = converter_sha256 if converter_sha256 is not None else sha('/usr/bin/sips') + require(rec['converter_sha256'] == pin, 'converter SHA mismatch') + require(rec['argv'] == ['/usr/bin/sips', '-s', 'format', 'png', rec['raw_path'], '--out', rec['png_path']], + 'converter argv mismatch') + require(type(rec['exit_code']) is int and rec['exit_code'] == 0, 'converter exit code') + for key in ('stdout_sha256', 'stderr_sha256'): + require(isinstance(rec[key], str) and re.fullmatch('[0-9a-f]{64}', rec[key]), + 'invalid converter output digest') + require(sha(png) == rec['png_sha256'], 'converted PNG SHA mismatch') + pw, ph, rows = png_pixels(png.read_bytes()) + require((w, h) == (pw, ph) == (rec['width'], rec['height']), 'conversion dimensions mismatch') + return pw, ph, rows + + +def screenshot_pixels(path, conversion=None, cua_receipt=None, converter_sha256=None): + data = Path(path).read_bytes() + kind, _, _ = image_kind(data) + if kind == 'jpeg': + require(conversion is not None, 'JPEG screenshot requires conversion record') + require(Path(conversion['raw_path']).resolve() == Path(path).resolve(), 'conversion raw path mismatch') + require(cua_receipt is not None, 'missing CUA receipt') + return inspect_conversion(conversion, cua_receipt, converter_sha256) + return png_pixels(data) + + +def affine_point(matrix, point): + require(len(matrix) == 6 and all(type(v) in (int, float) and math.isfinite(v) for v in matrix), 'invalid affine') + a, b, c, d, x, y = matrix + require(a*d-b*c != 0, 'singular affine') + return [a*point[0]+c*point[1]+x, b*point[0]+d*point[1]+y] + + +def affine_rect(matrix, rect): + x, y, w, h = rect + points = [affine_point(matrix, p) for p in ([x,y], [x+w,y], [x,y+h], [x+w,y+h])] + lo = [min(p[i] for p in points) for i in (0, 1)] + return lo + [max(p[i] for p in points)-lo[i] for i in (0, 1)] + + +def screenshot_transform(calibration, request, image_size, clicks): + """Content -> window screenshot pixels, cross-checked against actual clicks. + + This is independent of the renderer's content-space screen_rect. In + particular, window_position is a desktop coordinate, not an image origin. + No titlebar height, scale, or image extent is assumed by the checker. + """ + scale = calibration['backing_scale'] + stretch = calibration['canvas_stretch'] + titlebar = calibration['titlebar_pixels'] + require(all(type(v) in (int, float) and math.isfinite(v) for v in + (scale, stretch, titlebar)) and scale > 0 and stretch > 0 and titlebar >= 0, + 'invalid screenshot calibration') + close(calibration['window_size'], request['window_size'], 'calibrated window size') + close(calibration['window_position'], request['window_position'], 'calibrated window position') + close(calibration['screenshot_pixels'], list(image_size), 'calibrated image size') + close([request['screen_scale'], request['content_scale_factor']], [scale, stretch], + 'calibrated engine scale') + close([request['window_size'][0]*scale, request['window_size'][1]*scale+titlebar], + list(image_size), 'calibrated content extent') + matrix = [scale, 0, 0, scale, 0, titlebar] + matched = 0 + for receipt in clicks: + if receipt.get('target_pid') != request['target_pid']: + continue + for event in receipt['events']: + if event.get('action') != 'click': + continue + close(event['calibration'], calibration, 'click calibration') + point = event['point'] + close(affine_point(point['viewport_to_screen'], point['viewport']), + point['screen'], 'click content affine') + close(affine_point(matrix, point['screen']), event['coordinates'], + 'click screenshot offset') + matched += 1 + require(matched > 0, 'missing independent click calibration') + return matrix + + +def rect_intersection(a, b): + require(len(a) == len(b) == 4 and all(type(v) in (int, float) and math.isfinite(v) + for v in a+b) and min(a[2:]+b[2:]) >= 0, 'invalid clipping rectangle') + x, y = max(a[0], b[0]), max(a[1], b[1]) + return [x, y, max(0, min(a[0]+a[2], b[0]+b[2])-x), + max(0, min(a[1]+a[3], b[1]+b[3])-y)] + + +def rectangle_eligibility(rect, viewport): + clipped = rect_intersection(list(rect), [0, 0, *viewport]) + if not clipped[2] or not clipped[3]: + return 'outside', clipped + return ('visible' if clipped == list(rect) else 'clipped'), clipped + + +def paired_crop_noise(before, after, rect): + """Measured temporal differences, never an invented JPEG RGB tolerance. + + A stable pair alone does not establish class or state identity. The result + is diagnostic data only and must not be promoted to pixels_. + """ + require(before[:2] == after[:2], 'paired image dimensions') + x, y, w, h = rect_intersection(list(rect), [0, 0, *before[:2]]) + errors = [max(abs(a-b) for a, b in zip(before[2][py][px][:3], after[2][py][px][:3])) + for py in range(math.ceil(y), math.floor(y+h)) + for px in range(math.ceil(x), math.floor(x+w))] + require(errors, 'empty noise crop') + return dict(samples=len(errors), max_channel_delta=max(errors), + mean_channel_delta=sum(errors)/len(errors), unchanged=sum(v == 0 for v in errors)) + + +def drawing_rectangle_rows(report): + """Derive identities and local positions; do not read audit verdicts.""" + if any('anchor' in b for key in ('badges', 'landing_badges', 'actor_aggregations') + for b in report.get(key, [])): + validate_observation_labels(report) + expected = [] + tower = 'scripts/ui/pixel_tower_art.gd' + people = 'scripts/ui/pixel_people.gd' + world = 'scenes/main/world_preview.gd' + def actor(a, category=None): + expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'], + a['at'] + [16*a['scale'], 18*a['scale']], people, None)) + def badge(category, identifier, b): + expected.append((category, identifier, badge_rectangle(b['at'], b['text']), world, b['text'])) + for fid, room in report['rooms'].items(): + if 'rect' in room: + expected.append(('room', fid, room['rect'], tower, None)) + for i, b in enumerate(report['landing_badges']): + badge('queue', str(i), b) + for cabin in report['cabins']: + expected.append(('EV', cabin['shaft_id'], cabin['rect'], tower, None)) + for a in cabin['passengers']: + actor(a) + for a in report['actor_drawings']: + actor(a) + if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']: + actor(a, 'rest') + for i, b in enumerate(report.get('actor_aggregations', [])): + badge('aggregation', str(i), b) + for i, b in enumerate(report['badges']): + badge('goods' if b['kind'] == 'supply' else 'business', str(i), b) + for row in report.get('traffic', []): + if row['traffic']['current_density'] > 0 or row['traffic']['temporary_block']: + expected.append(('traffic', ','.join(map(str, row['cell'])), row['rect'], world, None)) + return expected + + +PIXEL_PEOPLE_CORE_SHA256 = 'a171703083aa2d00f8be2b63e1daef9c41717c8fe59d8056878bc4b7df1e0b0c' + + +def actor_identity_core(actor): + """Grid identity bounds from the pinned PixelPeople pose, including bob. + + Standard-room breathing extends the upper edge, not the lower edge. + Arms, legs, props and the sleep symbol are outside the identity core. + """ + key, at, scale = actor['key'], actor['at'], actor['scale'] + require(len(at) == 2 and all(type(v) in (int, float) and math.isfinite(v) for v in at) + and type(scale) in (int, float) and math.isfinite(scale) and scale > 0, + 'invalid actor footprint: ' + key) + require(isinstance(actor.get('mode'), str) and type(actor.get('frame')) is int + and type(actor.get('flip')) is bool, 'missing actor pose: ' + key) + mode, phase = actor['mode'], actor['frame'] % 8 + if mode in ('sleep', 'collapsed', 'standard_room_sleep'): + breathing = -1 if mode == 'standard_room_sleep' and phase in (2, 3, 4) else 0 + x, y, w, h = 2, 11 + breathing, 13, 5 - breathing + else: + bob = -1 if mode in ('walk', 'carry_walk') and phase in (2, 6) else 0 + x, y, w, h = 6, 1 + bob, 6, 12 + if actor['flip']: + x = 16 - x - w + return [at[0] + x * scale, at[1] + y * scale, w * scale, h * scale] + + +def validate_actor_nonoverlap(report, root=ROOT, transit_keys=()): + """Recompute identity cores from drawing inputs, including cabin passengers. + + Do not consume audit eligibility or boolean judgments: neither an off-screen + label nor a claimed pass exempts a submitted actor from this invariant. + Transparent margins may overlap. No asserted transit flag grants exemption. + """ + require(hashlib.sha256((root / 'scripts/ui/pixel_people.gd').read_bytes()).hexdigest() + == PIXEL_PEOPLE_CORE_SHA256, 'PixelPeople core source SHA256') + actors = list(report['actor_drawings']) + [ + actor for cabin in report['cabins'] for actor in cabin['passengers']] + rects = [] + keys = set() + for actor in actors: + key = actor['key'] + require(key not in keys, 'duplicate actor drawing: ' + key) + keys.add(key) + rect = actor_identity_core(actor) + if key in transit_keys: + continue + for other_key, other in rects: + intersects = (rect[0] < other[0] + other[2] and other[0] < rect[0] + rect[2] + and rect[1] < other[1] + other[3] and other[1] < rect[1] + rect[3]) + require(not intersects, 'actor-actor core overlap: ' + other_key + ' / ' + key) + rects.append((key, rect)) + + +def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None): + # Without raw inputs this is deliberately conservative: NO transit exemption. + # With inputs, use the same state-derived classification and fit proof as the + # stream checker, never a drawing's region/in_transit assertion. + if state is None: + validate_actor_nonoverlap(report, root) + else: + require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict), + 'malformed raw state context') + if report['resting_staff']: + require(rest_audit is not None, 'missing rest authority for drawing geometry') + if rest_audit is not None: + require(isinstance(rest_audit, dict) and isinstance(rest_audit.get('staff'), list), + 'malformed rest authority') + compare_rest(state, report, rest_audit, root) + compare_cabin_geometry(state, report) + compare_actor_geometry(state, report) + expected = drawing_rectangle_rows(report) + require(len(audit['rows']) == len(expected), 'offline rectangle coverage') + for row, (category, identifier, rect, source, text) in zip(audit['rows'], expected): + require((row['category'], row['id'], row['style_source']) == (category, identifier, source), + 'offline rectangle identity') + rect = list(rect) + if text is not None: + require(row['text'] == text and type(row['font_width']) in (int, float) + and math.isfinite(row['font_width']) and row['font_width'] > 0, 'offline font/text') + require(row['font_width'] <= rect[2] - 6, 'badge text exceeds reproducible rectangle') + close(row['rect'], rect, 'offline report drawing rectangle') + colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text()))) + require(row['style_colors'] == colors and colors, 'offline source palette provenance') + + +def encode_png(rows): + """Lossless review crop from decoded pixels, without an imaging dependency.""" + require(rows and rows[0], 'empty crop') + width, channels = len(rows[0]), len(rows[0][0]) + require(channels in (3, 4), 'crop channels') + def chunk(kind, body): + return (struct.pack('>I', len(body)) + kind + body + + struct.pack('>I', zlib.crc32(kind + body))) + raw = b''.join(b'\0' + bytes(v for pixel in row for v in pixel) for row in rows) + return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', struct.pack( + '>IIBBBBB', width, len(rows), 8, 2 if channels == 3 else 6, 0, 0, 0)) + + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')) + + +def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT, state=None, rest_audit=None): + """One mechanical path for frozen diagnostics and actual OS frames. + + Errors are retained as failures, including incomplete frozen audits. Every + supplied row is accounted for; no palette value is an image oracle. + """ + errors, rows = [], [] + transit_authority = ('raw state, track, time and pause' if state is not None + else 'absent: fail closed, no transit exemptions') + try: + validate_drawing_rows(report, audit, root, state, rest_audit) + except (ValueError, KeyError, TypeError) as exc: + errors.append(str(exc)) + calibration = screenshot_transform(receipt['calibration'], request, images[0][:2], clicks) + for image in images: + require(image[:2] == images[0][:2], 'frame image dimensions') + for point in ([0, 0], [1, 0], [0, 1]): + close(affine_point(request['viewport_to_screen'], affine_point(request['local_to_viewport'], point)), + affine_point(request['local_to_screen'], point), 'frame affine composition') + for index, row in enumerate(audit['rows']): + failures = list(errors) + close(row['viewport_rect'], affine_rect(request['local_to_viewport'], row['rect']), + 'frame viewport rectangle') + close(row['screen_rect'], affine_rect(request['local_to_screen'], row['rect']), + 'frame content rectangle') + status, clipped = rectangle_eligibility(row['rect'], report['viewport']) + screenshot_rect = affine_rect(calibration, affine_rect(request['local_to_screen'], clipped)) + room = report['rooms'].get(row['id']) if row['category'] == 'room' else None + if row['category'] in ('business', 'goods'): + room = report['rooms'].get(report['badges'][int(row['id'])]['facility']) + operation, state = None, None + if room is not None: + states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State') + operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState') + require(type(room['state']) is int and 0 <= room['state'] < len(states), 'frame room enum') + state = states[room['state']] + require(state == room['state_name'], 'frame room state name') + require(type(room['operation']) is int and 0 <= room['operation'] < len(operations), 'frame operation enum') + operation = operations[room['operation']] + actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']] + actor = next((a for a in actors if a['key'] == row['id']), None) + if actor is not None: + actor_states = enum_names(root, 'scripts/staff/staff_runtime.gd' if actor['staff'] + else 'scripts/guest/guest_runtime.gd', 'State') + require(type(actor['state']) is int and 0 <= actor['state'] < len(actor_states), + 'frame actor state enum') + state = actor_states[actor['state']] + if row['category'] == 'queue': + badge = report['landing_badges'][int(row['id'])] + state = {k: badge[k] for k in ('members', 'visible_count', 'overflow')} + if row['category'] == 'aggregation': + badge = report['actor_aggregations'][int(row['id'])] + state = {k: badge[k] for k in ('members', 'drawn_keys')} + if row['category'] == 'EV': + cabin = next(c for c in report['cabins'] if c['shaft_id'] == row['id']) + state = {k: cabin[k] for k in ('operational', 'doors_open')} + if row['category'] == 'traffic': + traffic = next(r['traffic'] for r in report.get('traffic', []) + if ','.join(map(str, r['cell'])) == row['id']) + state = {k: traffic[k] for k in ('current_density', 'temporary_block')} + result = dict(row_index=index, category=row['category'], id=row['id'], layer=report['layer'], + state=state, state_name=state if isinstance(state, str) else None, operation=operation, + dimmed=row['category'] == 'room' and operation != 'OPERATING', + text=row.get('text'), local_rect=row['rect'], screenshot_rect=screenshot_rect, + style_source=row['style_source'], style_source_sha256=sha(root/row['style_source']), + style_colors=row['style_colors'], font_width=row.get('font_width'), + eligibility=status, reason={'visible': 'whole rectangle in viewport', + 'clipped': 'rectangle intersects viewport', 'outside': 'no viewport intersection'}[status], + crops=[]) + if status != 'outside': + for number, (width, height, pixels) in enumerate(images): + x, y, w, h = rect_intersection(screenshot_rect, [0, 0, width, height]) + crop = [line[math.ceil(x):math.floor(x+w)] + for line in pixels[math.ceil(y):math.floor(y+h)]] + if not crop or not crop[0]: + failures.append('eligible crop outside image') + continue + if len({pixel for line in crop for pixel in line}) == 1: + failures.append('blank crop') + data = encode_png(crop) + result['crops'].append(dict(image_index=number, sha256=hashlib.sha256(data).hexdigest(), + png_bytes=data)) + result['mechanical_pass'] = not failures + result['failures'] = failures + rows.append(result) + return dict(rows=rows, errors=errors, mechanical_pass=not errors and all(r['mechanical_pass'] for r in rows), + expected_row_count=len(drawing_rectangle_rows(report)), + transit_authority=transit_authority) + + +def write_frame_review(directory, frame, provenance): + directory = Path(directory) + directory.mkdir(parents=True, exist_ok=True) + manifest = copy.deepcopy(frame) + manifest['images'] = provenance + for row in manifest['rows']: + for crop in row['crops']: + name = 'row-%04d-image-%d.png' % (row['row_index'], crop['image_index']) + (directory/name).write_bytes(crop.pop('png_bytes')) + crop['path'] = name + data = (json.dumps(manifest, sort_keys=True, ensure_ascii=False, indent=2) + '\n').encode() + (directory/'manifest.json').write_bytes(data) + digest = hashlib.sha256(data).hexdigest() + (directory/'manifest.sha256').write_text(digest + '\n') + return manifest, digest + + +def reviewed_pixel_categories(manifest, digest, verdict_path, *, require_judgments=False): + """The worker never writes verdicts; absent independent review earns nothing.""" + if not Path(verdict_path).is_file(): + return set() + verdict = json.loads(Path(verdict_path).read_bytes()) + require(verdict['manifest_sha256'] == digest, 'review manifest SHA mismatch') + require(verdict['reviewer'] == 'independent-opus-direct-image', 'independent image reviewer required') + if require_judgments: + require(isinstance(verdict.get('judgments'), list), 'explicit indexed judgments required') + verdict = dict(verdict, rows=verdict['judgments']) + eligible = [r for r in manifest['rows'] if r['eligibility'] != 'outside'] + require([v['row_index'] for v in verdict['rows']] == [r['row_index'] for r in eligible], + 'review row coverage') + for row, decision in zip(eligible, verdict['rows']): + require(all(decision[k] is True for k in ('category', 'state', 'position')), 'review row rejected') + if not manifest['mechanical_pass']: + return set() + return {r['category'] for r in eligible if r['mechanical_pass']} + + +def mirrored_inventory(directory): + """Read every indexed byte; preserve original receipt paths as authority.""" + directory = Path(directory).resolve() + index = json.loads((directory/'MIRROR_INDEX.json').read_bytes()) + require(Path(index['mirror']).resolve() == directory, 'mirror root mismatch') + paths, mapping = set(), [] + source = Path(index['source']) + for row in index['files']: + name = Path(row['path']) + require(not name.is_absolute() and '..' not in name.parts and str(name) not in paths, + 'invalid/duplicate mirror path') + paths.add(str(name)) + local = directory/name + require(local.resolve().is_relative_to(directory) and not local.is_symlink(), 'mirror escape') + data = local.read_bytes() + digest = hashlib.sha256(data).hexdigest() + require(digest == row['sha256'] and len(data) == row['bytes'], 'mirror hash/size mismatch') + mapping.append(dict(original=str(source/name), mirror=str(local), sha256=digest)) + prior = index['previous_inventory'] + prior_name = Path(prior['path']) + require(not prior_name.is_absolute() and '..' not in prior_name.parts + and (directory/prior_name).resolve().is_relative_to(directory) + and not (directory/prior_name).is_symlink(), 'previous inventory escape') + require(sha(directory/prior['path']) == prior['sha256'], 'previous mirror inventory hash') + old = json.loads((directory/prior['path']).read_bytes()) + require(all(row in index['files'] for row in old['files']), 'previous mirror entries changed') + return index, mapping + + +def inspect_offline_mirror(directory): + """Preparation diagnostics, deliberately separate from the acceptance gate. + + Frozen incomplete draw audits cannot acquire missing rows by relabelling + source-derived rectangles as observed engine output. + """ + directory = Path(directory).resolve() + index, mapping = mirrored_inventory(directory) + by_original = {row['original']: row for row in mapping} + def resolve(original): + require(original in by_original, 'unindexed original path') + return Path(by_original[original]['mirror']) + pair = directory/'os-pair/A' + clicks = [json.loads((pair/name/'cua-receipt.json').read_bytes()) + for name in ('writer-request-0001', 'writer-request-0003')] + results, inspected = [], [] + for name in ('writer-request-0002', 'writer-request-0004'): + folder = pair/name + request = json.loads((folder/'request.json').read_bytes()) + receipt = json.loads((folder/'cua-receipt.json').read_bytes()) + require(sha(folder/'request.json') == receipt['request_sha256'], 'offline request binding') + require(receipt['target_pid'] == request['target_pid'], 'offline screenshot PID') + report, audit = decode(request['report']), decode(request['rectangles']) + images = [] + for number, suffix in enumerate(('', '-after')): + conversion_path = folder/('root-early-conversion'+suffix+'.json') + conversion = json.loads(conversion_path.read_bytes()) + shot = receipt['screenshots'][number] + require(resolve(conversion['raw_path']) == directory/shot['path'], 'offline raw path binding') + image = inspect_conversion(conversion, shot, resolve_path=resolve) + for stream in ('stdout', 'stderr'): + require(sha(Path(str(conversion_path)+'.'+stream)) == conversion[stream+'_sha256'], + 'offline converter log hash') + images.append(image) + for original in (str(Path(index['source'])/conversion_path.relative_to(directory)), + conversion['png_path']): + inspected.append(by_original[original]) + stream = pair/'writer-stream' + captures = [row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines()) + if row.get('label') == name and 'report' in row] + require(len(captures) == 1, 'offline raw-state capture coverage') + capture = captures[0] + state, rest_audit = drawing_context(stream, capture, report) + frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT, + state, rest_audit) + provenance = [] + for number, suffix in enumerate(('', '-after')): + conversion_path = folder/('root-early-conversion'+suffix+'.json') + conversion = json.loads(conversion_path.read_bytes()) + provenance.append(dict(whole_image_sha256=conversion['png_sha256'], + raw_sha256=conversion['raw_sha256'], + conversion_sha256=sha(conversion_path), + request_sha256=sha(folder/'request.json'), + receipt_sha256=sha(folder/'cua-receipt.json'))) + review_dir = ROOT/'docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review'/name + manifest, digest = write_frame_review(review_dir, frame, provenance) + credited = reviewed_pixel_categories(manifest, digest, review_dir/'independent-verdict.json', require_judgments=True) + present = {r['category'] for r in frame['rows'] if r['eligibility'] != 'outside'} + overlays = [dict(cell=r['cell'], rect=r['rect']) for r in report['traffic'] + if (r['traffic']['current_density'] > 0 or r['traffic']['temporary_block']) + and not any(a['category'] == 'traffic' and a['id'] == ','.join(map(str, r['cell'])) + for a in audit['rows'])] + results.append(dict(request=name, layer=report['layer'], manifest_sha256=digest, + manifest_path=str(review_dir/'manifest.json'), rows=manifest['rows'], + mechanical_pass=frame['mechanical_pass'], errors=frame['errors'], + unaudited_traffic_overlays=overlays, + pending_categories=[c for c in CATEGORIES if c not in present], + pixel_categories=sorted(credited), independent_review='pending')) + return dict(task_id='P23-03', acceptance='pending', status='blocked', + inventory_count=len(mapping), path_map=mapping, inspected_eight=inspected, + frames=results, root_native_os_full_regression_independent_review='pending') + + +def enum_names(root, relative, name): + source = (root / relative).read_text() + match = re.search(r'\benum\s+' + re.escape(name) + r'\s*\{([^}]+)\}', source) + require(match is not None, 'missing source enum ' + name) + names = [x.strip() for x in match[1].split(',') if x.strip()] + require(all(re.fullmatch(r'[A-Z][A-Z_0-9]*', x) for x in names), 'unsupported enum schema') + require(len(set(names)) == len(names), 'duplicate source enum') + return names + + +def projection(rows, mapping, states): + result = [] + ids = set() + for row in rows: + value = {target: copy.deepcopy(row[source]) for target, source in mapping.items()} + require(value['id'] not in ids, 'duplicate actor/task ID') + ids.add(value['id']) + require(value['state'] in states, 'unknown typed state') + result.append(value) + return result + + +def compare_actor_rows(snapshot, report, root=ROOT): + runtime = snapshot['systems']['runtime'] + guest_states = enum_names(root, 'scripts/guest/guest_runtime.gd', 'State') + staff_states = enum_names(root, 'scripts/staff/staff_runtime.gd', 'State') + task_states = enum_names(root, 'scripts/staff/staff_task.gd', 'State') + mappings = ( + ('guests', runtime['guests']['guests'], guest_states, + dict(id='guest_id', state='state', cell='current_cell', target='target_facility_id', + service='current_service_id', queue='queue_request_id')), + ('staff', runtime['staff']['staff'], staff_states, + dict(id='staff_id', state='state', cell='current_cell', task='current_task_id', + facility='assigned_facility_id', role='role_id', carried_inventory_id='carried_inventory_id')), + ('tasks', runtime['staff']['tasks'], task_states, + dict(id='task_id', state='state', type='task_type', staff='assigned_staff_id', + target='target_entity_id', rescue_facility='rescue_facility_id')), + ) + for key, rows, states, mapping in mappings: + expected = projection(rows, mapping, states) + require(report[key] == expected, 'typed projection mismatch: ' + key) + return [key for key, *_ in mappings] + + +def elevator_report(snapshot): + saved = snapshot['systems']['runtime']['elevators'] + # ElevatorService.restore_snapshot validates this exact trip shape (412-438). + # QUEUED means no cabin seat yet, not an absent shaft assignment. + configs = {shaft['config']['shaft_id']: shaft['config'] for shaft in saved['shafts']} + for trip in saved['trips']: + keys = {'actor_id', 'request_id', 'shaft_id', 'connector_id', 'from', 'to', + 'enqueued_at', 'state'} + if 'companion_id' in trip: + keys.add('companion_id') + require(isinstance(trip['companion_id'], str) and trip['companion_id'], + 'invalid elevator companion identity') + require(set(trip) == keys, 'exact elevator trip keys') + require(trip['shaft_id'] in configs, 'elevator trip shaft attribution') + config = configs[trip['shaft_id']] + require(trip['from'] != trip['to'] and trip['from'] in config['stops'] + and trip['to'] in config['stops'], 'elevator trip stops') + low, high = sorted((trip['from'][1], trip['to'][1])) + require(trip['connector_id'] == f"{trip['shaft_id']}.link.{low}.{high}", + 'elevator trip connector attribution') + require(trip['state'] in ('QUEUED', 'BOARDED'), 'elevator trip state') + rows = [] + for shaft in saved['shafts']: + row = copy.deepcopy(shaft) + row.update(shaft['config']) + floor = float(shaft['current_floor']) + if shaft['phase'] in ('PICKUP', 'TRAVEL'): + destination = shaft['boarding_floor'] if shaft['phase'] == 'PICKUP' else shaft['target_floor'] + progress = 1.0 - shaft['remaining_minutes'] / max(float(shaft['phase_total_minutes']), 1.0) + floor = shaft['phase_origin_floor'] + (destination - shaft['phase_origin_floor']) * progress + row.update(visual_floor=floor, passenger_count=len(shaft['boarded']), + passenger_ids=copy.deepcopy(shaft['boarded']), + # get_report() merges config before counting (345-359). + waiting_count=sum(t['shaft_id'] == row['shaft_id'] and t['state'] == 'QUEUED' + for t in saved['trips'])) + rows.append(row) + # WorldPreview sorts its report's trip copy for queue placement. + trips = sorted(copy.deepcopy(saved['trips']), key=lambda t: (t['enqueued_at'], t['actor_id'])) + return dict(current_minute=saved['current_minute'], shafts=rows, trips=trips) + + +def close(actual, expected, label): + if isinstance(expected, (float, int)) and not isinstance(expected, bool): + require(type(actual) in (int, float) and math.isfinite(actual) + and math.isclose(actual, expected, abs_tol=0.0001, rel_tol=0.000001), label) + elif isinstance(expected, dict): + require(isinstance(actual, dict) and actual.keys() == expected.keys(), label + ' keys') + for key in expected: + close(actual[key], expected[key], label + '/' + str(key)) + elif isinstance(expected, list): + require(isinstance(actual, list) and len(actual) == len(expected), label + ' length') + for a, e in zip(actual, expected): + close(a, e, label) + else: + require(type(actual) is type(expected) and actual == expected, label) + + +class Transform: + """Independent port of the shared cell/map transform; no report rect input.""" + def __init__(self, runtime, report): + self.bounds = runtime['world']['bounds'] + b = self.bounds + columns, rows = b['max_x'] - b['min_x'] + 1, b['max_y'] - b['min_y'] + 1 + width, height = report['viewport'] + require(width > 0 and height > 0 and report['zoom'] > 0, 'camera dimensions') + fit = min(max(width - 152, 1) / (columns * 16), max(height - 140, 1) / (rows * 24)) + pixel = max(math.floor(fit), 1) if fit >= 1 else max(fit, .25) + self.dim = [max(math.floor(v * pixel * report['zoom'] + .5), minimum) + for v, minimum in ((16, 4), (24, 6))] + self.origin = [math.floor((extent - count * dim) / 2 + pan) + for extent, count, dim, pan in zip((width, height), (columns, rows), self.dim, report['pan'])] + + def rect(self, cell): + b = self.bounds + return [self.origin[0] + (cell[0] - b['min_x']) * self.dim[0], + self.origin[1] + (b['max_y'] - cell[1]) * self.dim[1], *self.dim] + + def center(self, cell): + x, y, w, h = self.rect(cell) + return [x + w / 2, y + h / 2] + + def room(self, cells): + rects = [self.rect(c) for c in cells] + x, y = min(r[0] for r in rects), min(r[1] for r in rects) + return [x, y, max(r[0] + r[2] for r in rects) - x, max(r[1] + r[3] for r in rects) - y] + + +def badge_rectangle(at, text): + return list(at) + [len(text) * 10 + 6, 14] + + +def observation_label_layout(report): + """Independent ordered rectangle placement; never consume reported at/reasons.""" + placed, result = [], [] + for i, cabin in enumerate(report['cabins']): + placed.extend([(f'cabin/{i}/rect', cabin['rect']), + (f'cabin/{i}/badge', badge_rectangle(cabin['badge_at'], cabin['badge']))]) + for kind, collection in [('business', 'badges'), ('supply', 'badges'), + ('landing', 'landing_badges'), ('aggregation', 'actor_aggregations')]: + for i, row in enumerate(report.get(collection, [])): + if collection == 'badges' and row['kind'] != kind: + continue + anchor = list(row.get('anchor', row['at'])) + at, reasons = anchor[:], [] + while True: + conflict = next(((identity, rect) for identity, rect in placed + if intersects(badge_rectangle(at, row['text']), rect)), None) + if conflict is None: + break + identity, rect = conflict + reasons.append(identity) + at[0] = rect[0] + rect[2] + result.append((collection, i, dict(anchor=anchor, at=at, displaced_by=reasons))) + placed.append((f'{kind}/{i}', badge_rectangle(at, row['text']))) + return result + + +def observation_anchor_report(report): + """Projection for existing raw-state semantic recomputation, not evidence.""" + import copy + raw = copy.deepcopy(report) + for collection in ('badges', 'landing_badges', 'actor_aggregations'): + for row in raw.get(collection, []): + if 'anchor' in row: + row['at'] = row.pop('anchor') + row.pop('displaced_by', None) + return raw + + +def validate_observation_labels(report): + require(all(key in report for key in ('cabins', 'badges', 'landing_badges', 'actor_aggregations')), + 'missing observation label collections') + rectangles = [badge_rectangle(c['badge_at'], c['badge']) for c in report['cabins']] + rectangles += [badge_rectangle(b['at'], b['text']) + for key in ('badges', 'landing_badges', 'actor_aggregations') + for b in report.get(key, [])] + for i, rect in enumerate(rectangles): + require(not any(intersects(rect, other) for other in rectangles[:i]), + 'observation label intersection') + for collection, index, wanted in observation_label_layout(report): + row = report[collection][index] + for key, value in wanted.items(): + require(key in row, 'missing label ' + key) + close(row[key], value, 'observation label/' + key) + + +def intersects(a, b): + cut = rect_intersection(a, b) + return cut[2] > 0 and cut[3] > 0 + + +def validate_cabin_intrusion(report): + for cabin in report['cabins']: + require(len(cabin['passengers']) <= cabin['passenger_count'], 'passenger drawn in empty cabin') + for actor in report.get('actor_drawings', []): + require(not intersects(actor_identity_core(actor), cabin['rect']), + 'non-passenger inside cabin: ' + actor['key']) + + +def traffic_obstacles(report): + obstacles = [] + for cabin in report['cabins']: + rect = badge_rectangle(cabin['badge_at'], cabin['badge']) + close(cabin['badge_rect'], rect, 'cabin badge rectangle') + obstacles.extend([cabin['rect'], rect]) + for collection in ('badges', 'landing_badges', 'actor_aggregations'): + obstacles.extend(badge_rectangle(b['at'], b['text']) for b in report.get(collection, [])) + return obstacles + + +def traffic_segments(band, cell, obstacles): + # Independent rectangle-set subtraction, including the deterministic fallback + # for a band entirely behind a cabin. Never accept self-asserted no-overlap. + for y in (band[1], cell[1], cell[1] + cell[3] - band[3]): + pieces = [[band[0], y, band[2], band[3]]] + for obstacle in obstacles: + remaining = [] + for x, top, w, h in pieces: + cut = rect_intersection([x, top, w, h], obstacle) + if not cut[2] or not cut[3]: + remaining.append([x, top, w, h]) + continue + left, upper, cw, ch = cut + remaining.extend(r for r in ([x, top, w, upper-top], + [x, upper+ch, w, top+h-upper-ch], [x, upper, left-x, ch], + [left+cw, upper, x+w-left-cw, ch]) if r[2] > 0 and r[3] > 0) + pieces = remaining + if pieces: + return pieces + return [] + + +def compare_traffic(snapshot, report): + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + rows = [] + for saved in sorted(runtime['path']['cell_traffic'], key=lambda r: (r['cell'][1], r['cell'][0])): + cell = saved['cell'] + if cell[2] != report['layer']: + continue + x, y, w, h = transform.rect(cell) + rect = [x + 2, y + 2, w - 4, h - 4] + thickness = max(2, min(5, (h - 4) * .16)) + band = [rect[0] + rect[2] * .12, y + h - 2 * (w / 16) - thickness / 2, + rect[2] * .76, thickness] + rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'}, + rect=rect, band_rect=band, + segment_rects=traffic_segments(band, rect, traffic_obstacles(report)))) + close(report['traffic'], rows, 'traffic and cell rect') + validate_life_overlay(report, snapshot=snapshot) + + +def validate_life_overlay(report, root=ROOT, snapshot=None): + """Check consumed frame and source wiring, not merely reported order flags.""" + serial = report.get('observation_serial') + require(type(serial) is int and serial > 0 + and type(report.get('overlay_consumed_serial')) is int + and report['overlay_consumed_serial'] == serial, 'stale LifeOverlay serial') + require(report.get('overlay_passes') == ['traffic', 'cabins', 'actors'], 'LifeOverlay pass order') + indices = report.get('overlay_child_indices', []) + require(len(indices) == 3 and all(type(i) is int and i >= 0 for i in indices) + and indices[0] < indices[1] < indices[2], 'LifeOverlay child order') + source = (root / 'scenes/main/world_preview.gd').read_text() + def body(name): + match = re.search(r'^func ' + name + r'\([^\n]*\n(.*?)(?=^func |\Z)', source, re.M | re.S) + require(match is not None, 'missing overlay source function: ' + name) + return match[1] + ready, draw, overlay = body('_ready'), body('_draw_contents'), body('_draw_life_overlay') + ordered = [ready.find('add_child(' + node + ')') + for node in ('_inactive_layer_art', '_life_overlay', '_layer_lens')] + require(all(i >= 0 for i in ordered) and ordered == sorted(set(ordered)), 'LifeOverlay source child order') + require('_life_overlay.draw.connect(_draw_life_overlay)' in ready + and '_life_overlay.queue_redraw()' in draw, 'LifeOverlay source connection') + require('_draw_congestion_overlay(' not in draw and '_draw_actors(' not in draw + and '_draw_observed_actor(' not in body('_draw_elevators'), 'actor or band drawn in parent') + require(0 <= overlay.find('_draw_congestion_overlay(_life_overlay)') + < overlay.find('_draw_elevators(_life_overlay)') + < overlay.find('_draw_life_badges(_life_overlay)') + < overlay.find('_draw_observed_actor(actor, _life_overlay)') + < overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order') + require('canvas.draw_rect(rect, Color("34413b"))' in body('_draw_readable_badge') + and 'canvas.draw_rect(row[&"band_rect"], Color(color, 0.90))' in body('_draw_congestion_overlay'), + 'occluded traffic requires opaque later badges and an actual band draw') + require('text.length() * 10 + 6' in body('_readable_badge_rect') + and '_readable_badge_rect(at, text)' in body('_draw_readable_badge') + and 'PixelArt.elevator_cabin(canvas,' in body('_draw_elevators') + and 'PixelArt.elevator_gate(canvas,' in body('_draw_elevators') + and '_draw_elevators(' not in draw, 'cabin/badge draw geometry or active-layer canvas differs') + actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']] + # Projection authority comes from raw state, never riding/region flags. + cabin_actors, carried_keys = set(), set() + if snapshot is not None: + if report['cabins']: + compare_cabin_geometry(snapshot, report) + shafts = {s['shaft_id']: s for s in elevator_report(snapshot)['shafts']} + for cabin in report['cabins']: + members = {key.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1) + for key in shafts[cabin['shaft_id']]['boarded']} + cabin_actors.update(id(a) for a in cabin['passengers'] if a['key'] in members) + tasks = snapshot['systems']['runtime']['staff']['tasks'] + carried_keys = {'staff/' + t['target_entity_id'] for t in tasks + if t['task_type'] == 'rescue_transport' + and t['state'] not in ('WORKING', 'COMPLETED', 'CANCELLED', 'FAILED') + and t['assigned_staff_id']} + carried_keys.difference_update(report.get('boarded', {}), report.get('queue_positions', {})) + if carried_keys: + # Reuse the original raw binding, track and raised-region fit checks. + compare_actor_geometry(snapshot, report) + obstacles = traffic_obstacles(report) + close(report['traffic_obstacles'], obstacles, 'traffic obstacle geometry') + validate_cabin_intrusion(report) + for row in report.get('traffic', []): + x, y, w, h = row['rect'] + thickness = max(2, min(5, h * .16)) + expected = [x + w * .12, y + h + 2 - 2 * ((w + 4) / 16) - thickness / 2, + w * .76, thickness] + close(row['band_rect'], expected, 'traffic band rectangle') + segments = traffic_segments(expected, row['rect'], obstacles) + close(row['segment_rects'], segments, 'traffic segment geometry') + require(all(not intersects(part, obstacle) for part in segments for obstacle in obstacles), + 'traffic intersects cabin or badge') + if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']: + continue + intervals = [] + for actor in actors: + core = actor_identity_core(actor) + overlaps = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2] + and core[1] < expected[1] + expected[3] and expected[1] < core[1] + core[3]) + require(not overlaps or id(actor) in cabin_actors + or (actor in report['actor_drawings'] and actor['key'] in carried_keys), + 'floor actor core over traffic band: ' + actor['key']) + # The relocated fragments also cannot cross an ordinary floor core. + require(not any(intersects(core, part) for part in segments) + or id(actor) in cabin_actors or actor['key'] in carried_keys, + 'floor actor core over traffic segment: ' + actor['key']) + if overlaps: + intervals.append((max(core[0], expected[0]), + min(core[0] + core[2], expected[0] + expected[2]))) + end = expected[0] + for left, right in sorted(intervals): + if left > end: + break + end = max(end, right) + require(end < expected[0] + expected[2], + 'traffic band fully occluded: ' + str(row.get('cell'))) + + +def compare_queues(snapshot, report): + if any('anchor' in b for b in report.get('landing_badges', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + requests = codec.indexed(runtime['facilities']['requests'], 'request_id') + guests = codec.indexed(runtime['guests']['guests'], 'guest_id') + groups = {} + def add(key, cell): + if cell[2] != report['layer']: + return + members = groups.setdefault(tuple(cell), []) + if key not in members: + members.append(key) + for facility in facilities.values(): + for rid in facility['queue_entries']: + request = requests.get(rid) + if request is None: + continue + for gid in request['guest_ids']: + guest = guests.get(gid) + if guest and guest['state'] == 'QUEUING': + add('guest/' + gid, guest['current_cell']) + elevators = elevator_report(snapshot) + for trip in elevators['trips']: + if trip['state'] == 'QUEUED': + add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from']) + footprint = list(dict.fromkeys((cell[0] + dx, cell[1], cell[2]) + for shaft in elevators['shafts'] for cell in shaft['stops'] + for dx in range(max(int(shaft.get('width', 2)), 1)))) + close(report['shaft_footprint'], [list(c) for c in footprint], 'shaft footprint') + lift_cells = set(footprint) + walkable = {tuple(cell) for cell in runtime['world']['walkable_cells']} + owners = {tuple(cell): row['entity_id'] for row in runtime['world']['occupancies'] for cell in row['cells']} + reserved, positions, hidden, badges = set(), {}, {}, [] + for cell in sorted(groups, key=lambda c: (c[1], c[0])): + members = groups[cell] + lane = [] if cell in lift_cells else [cell] + for direction in (-1, 1): + for distance in range(1, 9): + target = (cell[0] + direction * distance, cell[1], cell[2]) + facility = facilities.get(owners.get(target)) + if target in lift_cells or target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')): + break + lane.append(target) + index = 0 + for target in lane: + if index >= len(members): + break + if target in reserved: + continue + reserved.add(target) + positions[members[index]] = transform.center(target) + index += 1 + hidden.update({member: True for member in members[index:]}) + overflow = len(members) - index + rect = transform.rect(cell) + badges.append(dict(at=[rect[0] + (-80 if cell in lift_cells else 0), rect[1] - 16], + text=f'待ち{len(members)}人' + (f'(他{overflow}人)' if overflow else ''), + cell=list(cell), members=members, visible_count=index, overflow=overflow)) + for key, expected in [('queue_positions', positions), ('queue_hidden', hidden), ('landing_badges', badges)]: + close(report[key], expected, key) + + +def compare_room_services(snapshot, report, root=ROOT): + """Check authoritative service occupants and reasons; rest selection remains pending. + + Extra rest/rescue occupants are checked separately below, never silently + counted as service users. This does not claim the missing path-cost proof. + """ + runtime = snapshot['systems']['runtime'] + states = enum_names(root, 'scripts/facility/facility_instance.gd', 'State') + operations = enum_names(root, 'scripts/facility/facility_instance.gd', 'OperationState') + rooms = [r for r in runtime['facilities']['instances'] if r['origin'][2] == report['layer']] + treated, carried = {}, {} + for task in runtime['staff']['tasks']: + if task['task_type'] != 'rescue_transport' or task['state'] in ('COMPLETED', 'CANCELLED', 'FAILED') or not task['assigned_staff_id']: + continue + if task['state'] == 'WORKING': + treated[task['target_entity_id']] = task['rescue_facility_id'] + else: + carried[task['target_entity_id']] = task['assigned_staff_id'] + close(report['treated_patients'], treated, 'rescue treatment') + close(report['carried_patients'], carried, 'rescue carrier') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + all_rooms = codec.indexed(runtime['facilities']['instances'], 'instance_id') + for sid, fid in report['resting_staff'].items(): + require(sid in staff and fid in all_rooms, 'rest reference') + member, room = staff[sid], all_rooms[fid] + require(member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'], 'resting staff eligibility') + require(room['definition_id'] in ('staff_waiting_room', 'staff_dormitory') + and room['state'] in ('OPEN', 'DEGRADED') and not room['suspension_reason'], 'operating rest room') + require(set(report['rooms']) == {r['instance_id'] for r in rooms}, 'room identity') + for room in rooms: + observed = report['rooms'][room['instance_id']] + users = sorted({gid for service in room['active_services'] for gid in service['guest_ids']}) + users += [sid for sid in staff if report['resting_staff'].get(sid) == room['instance_id']] + users += [task['target_entity_id'] for task in runtime['staff']['tasks'] + if task['task_type'] == 'rescue_transport' and task['state'] == 'WORKING' + and task['assigned_staff_id'] and task['rescue_facility_id'] == room['instance_id']] + require(observed['people'] == users, 'service/rest/rescue occupant list') + require(observed['count'] == len(observed['people']), 'room count') + operation = 'SUSPENDED' if room['suspension_reason'] else ('OPERATING' if room['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE') + expected = dict(state=states.index(room['state']), state_name=room['state'], + reasons=room['state_reasons'], operation=operations.index(operation), + limit=room['customer_capacity'], staff=room['active_staff_ids'], suspension=room['suspension_reason']) + for key, value in expected.items(): + close(observed[key], value, 'room/' + key) + cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == room['instance_id']), []) + if cells: + close(observed['rect'], Transform(runtime, report).room(cells), 'room rect') + require(observed['definition_id'] == room['definition_id'], 'room definition') + + +def comparison_records(directory): + records = {} + for line in (directory / 'manifest.jsonl').read_text().splitlines(): + row = json.loads(line) + if 'object' not in row: + continue + value = read_object(directory, row['object']) + if row['label'] == 'event': + continue + require(row['label'] not in records, 'duplicate experiment record') + records[row['label']] = value + return records + + +def room_slots(rect, pixel, definition='staff_waiting_room'): + x, y, w, h = rect + step = 20 if definition in ('cafeteria', 'tavern') else 16 + columns = max(int(w / pixel / step), 1) + rows = max(int((h / pixel - (30 if definition == 'chapel' else 10)) / 20), 1) + if definition == 'chapel': + columns = min(columns, 4) + anchors = [[x + (col + .5) * w / columns, y + h - (4 + row * 20) * pixel] + for row in range(rows) for col in range(columns)] + return anchors[:8] if definition == 'chapel' else anchors + + +def compare_rest(state, report, audit, root=ROOT): + """Recompute selection, not shortest paths. Shortest-path authority is the + exact pinned PathService source producing the recorded public API paths. + Edge reports must be nonempty, including for temporarily closed cells. + """ + require(audit['path_service_sha256'] == sha(root / 'scripts/world/path_service.gd'), + 'rest PathService source pin') + runtime = state['systems']['runtime'] + members = runtime['staff']['staff'] + facilities = runtime['facilities']['instances'] + require([r['staff_id'] for r in audit['staff']] == [r['staff_id'] for r in members], + 'rest staff order/coverage') + passable = {tuple(r['cell']) for r in runtime['path']['cell_traffic']} + resting = {} + selections = {} + for member, row in zip(members, audit['staff']): + sid = member['staff_id'] + for key in ('state', 'health', 'current_task_id', 'current_cell'): + close(row[key], member[key], 'rest input/' + key) + memberships = [dict(raid_id=raid['raid_id'], state=raid['state'], side=c['side']) + for raid in runtime['defense']['raids'] for c in raid['combatants'] + if c['actor_id'] == sid] + close(row['defense_memberships'], memberships, 'rest defense membership') + locked = sid in runtime['events']['actor_locks'] + transit = any(t['actor_id'] == 'staff:' + sid for t in runtime['elevators']['trips']) + close(row['actor_lock'], locked, 'rest actor lock') + close(row['in_transit'], transit, 'rest transit') + defense = any(r['state'] not in ('DEFENDED', 'BREACHED', 'DEFENDERS_RETREATED', 'CANCELLED') + and r['side'] == 'DEFENDER' for r in memberships) + eligible = member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'] and not locked and not defense + selected = dict(reachable=False, resting=False, label='休憩先なし(稼働中の待機室・宿舎と経路を確認)') if eligible else {} + best_cost = 2147483647 + require([f['instance_id'] for f in row['facilities']] == [f['instance_id'] for f in facilities], + 'rest facility order/coverage') + for facility, observed in zip(facilities, row['facilities']): + for key in ('instance_id', 'definition_id', 'origin', 'occupied_cells'): + close(observed[key], facility[key], 'rest facility/' + key) + operation = 'SUSPENDED' if facility['suspension_reason'] else ('OPERATING' if facility['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE') + close(observed['operation_state'], operation, 'rest operation') + occupied = facility['occupied_cells'] or [facility['origin']] + probes = [dict(cell=[c[0] + dx, c[1], c[2]], has_cell=(c[0] + dx, c[1], c[2]) in passable) + for c in occupied for dx in (0, -1, 1)] + close(observed['probes'], probes, 'rest candidate probes') + ordered = sorted({tuple(p['cell']) for p in probes if p['has_cell']}, key=lambda c: (c[2], c[1], c[0])) + close(observed['ordered'], [list(c) for c in ordered], 'rest candidate order') + require([r['cell'] for r in observed['routes']] == [list(c) for c in ordered], 'rest route coverage/order') + best = None + for route in observed['routes']: + path, edges = route['path'], route['edges'] + require(len(edges) == max(len(path) - 1, 0), 'rest edge coverage') + if not path: + require(route['calculate_path_cost'] == -1, 'rest empty route cost') + continue + require(path[0] == member['current_cell'] and path[-1] == route['cell'], 'rest path endpoints') + require(all(tuple(c) in passable for c in path), 'rest path cell absent') + cost = 0 + for a, b, edge in zip(path, path[1:], edges): + require(edge and edge['from'] == a and edge['to'] == b, 'rest empty or discontinuous edge report') + require(type(edge['total_cost']) is int and edge['total_cost'] >= 0, 'rest edge cost') + cost += edge['total_cost'] + require(cost == route['calculate_path_cost'], 'rest edge sum differs from calculate_path_cost') + if best is None or cost < best[0]: + best = (cost, path[-1]) + if not eligible or operation != 'OPERATING' or facility['definition_id'] not in ('staff_waiting_room', 'staff_dormitory') or best is None or best[0] >= best_cost: + continue + best_cost = best[0] + cell = member['current_cell'] + at = any(c[2] == cell[2] and c[1] == cell[1] and abs(c[0] - cell[0]) <= 1 for c in occupied) and not transit + selected = dict(reachable=True, resting=at, cell=best[1], facility_id=facility['instance_id'], + label=('宿舎で休憩中' if facility['definition_id'] == 'staff_dormitory' else '待機室で休憩中') if at else '休憩先へ移動中') + close(row['comparison_only'], selected, 'rest selection comparison') + selections[sid] = selected + if selected.get('resting'): + resting[sid] = selected['facility_id'] + close(report['resting_staff'], resting, 'draw resting_staff') + # Fitting is cross-kind, and queue precedence applies to resting staff too. + # Reconstruct the complete group and independently prove p=1 no-fit before + # allowing an identity to appear only in an aggregation's member list. + if any(r['cells'] for r in runtime['world']['occupancies'] + if r['entity_id'] in resting.values()): + compare_actor_geometry(state, report) + return selections + + +def compare_goods(snapshot, report, root=ROOT): + runtime = snapshot['systems']['runtime'] + states = enum_names(root, 'scripts/inventory/logistics_request.gd', 'State') + candidates = codec.indexed(json.loads((root / 'data/characters/mvp_staff_candidates.json').read_text())['entries'], 'id') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + facilities = runtime['facilities']['instances'] + def owner(inventory): + return next((f['instance_id'] for f in facilities if f['local_inventory_id'] == inventory), '') + rows, active, recent = [], [], [] + for request in sorted(runtime['logistics_requests'], key=lambda r: r['request_id']): + require(request['state'] in states, 'logistics enum') + task = next((t for t in runtime['staff']['tasks'] + if t['target_entity_id'] == request['request_id'] + and t['task_type'] in ('supply_pickup', 'supply_delivery', 'supply_return') + and t['state'] not in ('COMPLETED', 'CANCELLED', 'FAILED')), None) + carrier = request['assigned_carrier_id'] + row = dict(source_facility_id=owner(request['source_inventory_id']), + destination_facility_id=owner(request['destination_inventory_id']), + carrier_name=candidates.get(carrier, {}).get('display_name', carrier) if carrier in staff else '', + reason_codes=[request['state_reason']] if request['state_reason'] else [], + request_id=request['request_id'], state=request['state'], reason=request['state_reason'], + carrier_id=carrier, source_inventory_id=request['source_inventory_id'], + destination_inventory_id=request['destination_inventory_id'], + item_requirements=request['item_requirements'], weight=request['total_weight'], + retry_count=request['retry_count'], task_id=task['task_id'] if task else '') + rows.append(row) + if request['state'] in ('COMPLETED', 'CANCELLED'): + recent.append((request['state_changed_at'], request['request_id'], row)) + else: + active.append(row) + recent.sort(key=lambda r: (r[0], r[1]), reverse=True) + close(report['goods'], dict(requests=rows, deliveries=active + [r[2] for r in recent[:10]]), 'goods') + + +def compare_badges(state, report): + if any('anchor' in b for b in report.get('badges', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = state['systems']['runtime'] + transform = Transform(runtime, report) + expected = [] + labels = dict(BROKEN='破損', NO_ACCESS='通路なし', NO_POWER='魔力なし', NO_STAFF='職員不足', + CLOSED='営業停止', DEGRADED='低下運転', OPEN='営業中') + for facility in runtime['facilities']['instances']: + fid = facility['instance_id'] + if facility['origin'][2] != report['layer']: continue + cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), []) + if not cells: continue + x,y,w,h = transform.room(cells) + room = report['rooms'][fid] + count, limit = room['count'], facility['customer_capacity'] + if facility['suspension_reason']: + business = '休止・摘発' if facility['suspension_reason'] == 'RAIDED' else '休止・破壊' + label = business + else: + business = labels.get(facility['state'], facility['state']) + if facility['state'] not in ('OPEN', 'DEGRADED'): + label = '未稼働' + else: + label = f'利用{count}/{limit}人' if limit >= 0 else f'利用{count}人' + if limit > 0 and count >= limit: label = '満員 ' + label + visible = len(room_slots([x,y,w,h], transform.dim[0]/16, facility['definition_id'])) + if count > visible: label += f'(他{count-visible}人)' + expected.append(dict(facility=fid, kind='business', at=[x+2,y+2], text=business+' '+label)) + requests = [r for r in report['goods']['requests'] if fid in (r['source_facility_id'], r['destination_facility_id'])] + if requests: + states = {} + for r in requests: states[r['state']] = states.get(r['state'], 0)+1 + text = '物資依頼 ' + ' / '.join(f'{k}:{states[k]}' for k in sorted(states)) + expected.append(dict(facility=fid, kind='supply', at=[x+2,y+16], requests=requests, text=text)) + close(report['badges'], expected, 'business/supply badges') + + +def compare_cabin_geometry(snapshot, report): + runtime = snapshot['systems']['runtime'] + transform = Transform(runtime, report) + expected = [] + p = transform.dim[0] / 16 + for shaft in elevator_report(snapshot)['shafts']: + if not shaft['stops'] or shaft['stops'][0][2] != report['layer']: + continue + floor = shaft['visual_floor'] + track = report['elevator_tracks'][shaft['shaft_id']] + close(track['floor'], floor, 'authoritative elevator track endpoint') + require(track['at'] <= report['animation_time'], 'elevator track time') + if not report['paused']: + # The prior-frame origin still needs a chained visual-history proof. + t = max(0, min(1, (report['animation_time'] - track['at']) / .42)) + floor = track['from'] + (floor - track['from']) * t + x, y, w, h = transform.rect(shaft['stops'][0]) + baseline = y + h - (floor - shaft['stops'][0][1]) * transform.dim[1] - 3 * p + def rounded(v): + return math.copysign(math.floor(abs(v) + .5), v) + rect = [rounded(x + 4*p), rounded(baseline - 19*p), max(shaft.get('width', 2), 1)*w - 8*p, 19*p] + if rect[0] >= report['viewport'][0] or rect[1] >= report['viewport'][1] or rect[0]+rect[2] <= 0 or rect[1]+rect[3] <= 0: + continue + count, capacity = len(shaft['boarded']), shaft['capacity'] + badge = f'{count}/{capacity}人' + if capacity > 0 and count >= capacity: + badge = '満員 ' + badge + if not shaft['operational']: + badge = '停止 ' + badge + else: + badge = '運転 ' + badge + doors = shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING') + badge += (' 開 ' if doors else ' 閉 ') + shaft['phase'] + expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'], + phase=shaft['phase'], passenger_count=count, doors_open=doors, + badge_rect=badge_rectangle([rect[0], rect[1]-14], badge), + badge_at=[rect[0], rect[1]-14], badge=badge)) + require(len(report['cabins']) == len(expected), 'cabin count') + for row, wanted in zip(report['cabins'], expected): + for key, value in wanted.items(): + close(row[key], value, 'cabin/' + key) + validate_cabin_intrusion(report) + + +def compare_motion(state, report, audit): + frames = audit['frames'] + require(len(frames) == 2, 'two motion history frames required') + previous, current = frames + require(type(previous['frame']) is int and current['frame'] > previous['frame'], 'motion frame order') + require(current['delta'] >= 0 and current['scaled_delta'] >= 0, 'motion delta') + before, after = previous['display'], current['display'] + for key, value in after.items(): + close(report[key], value, 'motion report binding/' + key) + close(after['animation_time'], before['animation_time'] + current['scaled_delta'], 'motion animation delta') + if before['paused'] and after['paused']: + close(current['scaled_delta'], 0, 'paused animation moved') + now = after['animation_time'] + runtime = state['systems']['runtime'] + cells = {'guest/' + r['guest_id']: r['current_cell'] for r in runtime['guests']['guests']} + cells.update({'staff/' + r['staff_id']: r['current_cell'] for r in runtime['staff']['staff']}) + def interpolated(track, endpoint, paused, transform=None): + t = 1 if paused else max(0, min(1, (now-track['at'])/.42)) + if transform: + a, b = transform.center(track['from']), transform.center(endpoint) + return [a[i] + (b[i]-a[i])*t for i in (0, 1)] + return track['from']+(endpoint-track['from'])*t + for key, track in after['actor_tracks'].items(): + require(key in cells and track['cell'] == cells[key], 'motion actor endpoint') + old = before['actor_tracks'].get(key) + if old is None: + wanted = dict(cell=cells[key], **{'from': cells[key]}, at=now, direction=1.0) + elif old['cell'] == cells[key]: + wanted = old + else: + a, b = old['cell'], cells[key] + origin = a if a[2] == b[2] and abs(a[0]-b[0])+abs(a[1]-b[1]) == 1 else b + wanted = dict(cell=b, **{'from': origin}, at=now, + direction=(1.0 if b[0] > a[0] else -1.0) if a[0] != b[0] else old['direction']) + close(track, wanted, 'actor interpolation origin/history') + # Ordinary walking centers; queue/room/rescue/working projections have + # independent semantic placement checks and do not use this center. + drawing = next((a for a in after['actor_drawings'] if a['key'] == key), None) + if drawing and drawing['mode'] in ('walk', 'carry_walk') and key not in report['queue_positions']: + position = interpolated(track, cells[key], after['paused'], Transform(runtime, report)) + # Same-cell crowd offsets are handled by the complete actor check. + require(all(math.isfinite(v) for v in position), 'nonfinite interpolation') + floors = {s['shaft_id']: s['visual_floor'] for s in elevator_report(state)['shafts']} + for key, track in after['elevator_tracks'].items(): + require(key in floors, 'unknown elevator motion') + old = before['elevator_tracks'].get(key) + if old is None: + wanted = dict(floor=floors[key], **{'from': floors[key]}, at=now) + elif math.isclose(old['floor'], floors[key], abs_tol=.00001, rel_tol=.00001): + wanted = old + else: + wanted = dict(floor=floors[key], **{'from': old['floor']}, at=now) + close(track, wanted, 'elevator interpolation origin/history') + if before['paused'] and after['paused'] and all(before[k] == after[k] for k in ('layer', 'zoom', 'pan', 'viewport', 'actor_tracks', 'elevator_tracks')): + prior_actors = {r['key']: r for r in before['actor_drawings']} + for row in after['actor_drawings']: + old = prior_actors.get(row['key']) + if old and all(old[k] == row[k] for k in ('state', 'mode', 'activity', 'receiving', 'carrying')): + close(row['at'], old['at'], 'paused actor moved') + return ['motion_history', 'interpolation', 'pause'] + + +def fitted_actor_layout(members, world_pixel, height): + """Independent interval solver. Inputs are reconstructed preferred rows/bounds.""" + def scaled(member, scale): + row = copy.deepcopy(member['row']) + row['scale'] = min(row['scale'], scale) + s = row['scale'] + row['at'] = [math.copysign(math.floor(abs(v)+.5), v) for v in + (row['center'][0]-8*s, row['center'][1]+height/2-3*world_pixel-17*s)] + return row + def pack(group, scale): + trial = [scaled(m, scale) for m in group] + right = -math.inf + for member, row in zip(group, trial): + x, y, w, h = actor_identity_core(row) + left = max(x, member['bounds'][0], right) + row['at'][0] += left-x + right = left+w + left = math.inf + for member, row in reversed(list(zip(group, trial))): + x, y, w, h = actor_identity_core(row) + bx, by, bw, bh = member['bounds'] + target = min(x, bx+bw-w, left-w) + row['at'][0] += target-x + left = target + if target < bx or y < by or y+h > by+bh: + return None + return trial + active = copy.deepcopy([m for m in members if not m['transit']]) + # A smaller sprite keeps its feet, so its vertical interval moves. Rebuild + # components after EVERY trial, including unsuccessful trials. Scale ceilings + # only decrease, making termination independent of an arbitrary iteration cap. + while True: + groups, bottom = [], -math.inf + for member in active: + member['row'] = scaled(member, member['row']['scale']) + for member in sorted(active, key=lambda m: (actor_identity_core(m['row'])[1], m['row']['key'])): + x, y, w, h = actor_identity_core(member['row']) + if y >= bottom: + groups.append([]) + bottom = y+h + else: + bottom = max(bottom, y+h) + groups[-1].append(member) + reduced = False + for group in groups: + group.sort(key=lambda m: (m['row']['center'][0], m['row']['key'])) + ceiling = max(1, *(m['row']['scale'] for m in group)) + if pack(group, ceiling) is None and ceiling > 1: + for member in group: + member['row'] = scaled(member, max(1, int(ceiling)-1)) + reduced = True + if not reduced: + break + result = {m['row']['key']: copy.deepcopy(m['row']) for m in members if m['transit']} + aggregations = [] + for group in groups: + group.sort(key=lambda m: (m['row']['center'][0], m['row']['key'])) + base = max(1, *(m['row']['scale'] for m in group)) + fitted = pack(group, base) + if fitted is None: + retained, fitted, identities = [], [], [] + for member in group: + row = member['row'] + identities.append(dict(key=row['key'], id=row['key'].split('/', 1)[1], + state=row['state'], cell=row['cell'], mode=row['mode'])) + attempt = pack(retained+[member], 1) + if attempt is not None: + retained.append(member) + fitted = attempt + aggregations.append(dict(members=identities, drawn_keys=[r['key'] for r in fitted], + at=group[0]['bounds'][:2], text='+'+str(len(group)-len(fitted)))) + result.update({row['key']: row for row in fitted}) + return result, aggregations + + +def actor_track_transit(report, key, cell): + """Track endpoint is bound to raw state by the caller, not to a draw flag.""" + require(type(report['paused']) is bool, 'actor pause must be boolean') + track = report['actor_tracks'][key] + close(track['cell'], cell, 'actor authoritative cell') + origin = track['from'] + require(isinstance(origin, list) and len(origin) == 3 + and all(type(v) is int for v in origin) + and origin[2] == cell[2] + and abs(origin[0]-cell[0])+abs(origin[1]-cell[1]) <= 1, + 'actor track is not a source-valid adjacent step') + now, started = report['animation_time'], track['at'] + require(all(type(v) in (int, float) and math.isfinite(v) for v in (now, started)) + and now >= started, 'invalid or future actor track time') + return not report['paused'] and origin != cell and now-started < .42 + + +def landing_actor_region(state, report, transform, cell, region, transit): + """Derive corridor landings from raw topology, never from actor flags.""" + if not any(intersects(region, cabin['rect']) for cabin in report['cabins']): + return dict(bounds=region, transit=transit) + runtime = state['systems']['runtime'] + footprint = {(stop[0]+dx, stop[1], stop[2]) + for shaft in elevator_report(state)['shafts'] for stop in shaft['stops'] + for dx in range(max(int(shaft.get('width', 2)), 1))} + walkable = {tuple(c) for c in runtime['world']['walkable_cells']} + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + owners = {tuple(c): r['entity_id'] for r in runtime['world']['occupancies'] for c in r['cells']} + distance = lambda c: (abs(c[0]-cell[0]), c[0]) + origins = sorted((c for c in footprint if c[1:] == tuple(cell[1:])), key=distance) + lane = set() + for origin in origins: + for direction in (-1, 1): + for step in range(1, 9): + target = (origin[0]+direction*step, origin[1], origin[2]) + facility = facilities.get(owners.get(target)) + if (target in footprint or target not in walkable or + (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor'))): + break + lane.add(target) + lane = sorted(lane, key=distance) + if not lane: + rect = transform.rect(cell) + badges = [b for b in report['landing_badges'] if b['cell'][1:] == list(cell[1:])] + anchor = min(badges, key=lambda b: distance(b['cell']))['at'] if badges else [rect[0]-80, rect[1]-16] + return dict(bounds=region, transit=False, no_lane=True, badge_at=anchor) + reserved = list(report['queue_positions'].values()) + selected = next((c for c in lane if transform.center(c) not in reserved), lane[0]) + return dict(bounds=transform.rect(selected), transit=False, center=transform.center(selected)) + + +def compare_actor_geometry(state, report): + if any('anchor' in b for b in report.get('actor_aggregations', [])): + validate_observation_labels(report) + report = observation_anchor_report(report) + runtime = state['systems']['runtime'] + compare_queues(state, report) + compare_room_services(state, report) + boarded = {actor.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1): True + for shaft in elevator_report(state)['shafts'] for actor in shaft['boarded']} + close(report['boarded'], boarded, 'boarded actor coverage') + transform = Transform(runtime, report) + p, height = transform.dim[0]/16, transform.dim[1] + facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id') + staff = codec.indexed(runtime['staff']['staff'], 'staff_id') + occupancy = {r['entity_id']: r['cells'] for r in runtime['world']['occupancies']} + # Cabin rectangles and badges are independently recomputed before use below. + if report['cabins']: + compare_cabin_geometry(state, report) + counts, expected = {}, [] + def track_position(key, cell): + track = report['actor_tracks'][key] + actor_track_transit(report, key, cell) + t = 1 if report['paused'] else max(0, min(1, (report['animation_time']-track['at'])/.42)) + a, b = transform.center(track['from']), transform.center(cell) + return [a[i]+(b[i]-a[i])*t for i in (0, 1)] + def offset(cell, center): + key = tuple(cell) + n = counts.get(key, 0) + counts[key] = n+1 + return [center[0]+[0,4,-4,2,-2,6,-6][n%7]*p, center[1]-(n%2)*p] + def slot_position(fid, identifier, definition, fallback, dy, missing_fallback=False): + cells = occupancy.get(fid, []) + if not cells: + return fallback + users = report['rooms'].get(fid, {}).get('people', []) + if identifier not in users: + return fallback if missing_fallback else None + anchors = room_slots(transform.room(cells), p, definition) + index = users.index(identifier) + return [anchors[index][0], anchors[index][1]+dy] if index < len(anchors) else None + people = {'guest/'+r['guest_id']: r for r in runtime['guests']['guests']} + people.update({'staff/'+sid: r for sid, r in staff.items()}) + regions = {} + def actor_region(key): + if key in regions: + return regions[key] + person = people[key] + cell = person['current_cell'] + rect = transform.rect(cell) + transit = actor_track_transit(report, key, cell) + if transit: + other = transform.rect(report['actor_tracks'][key]['from']) + x, y = min(rect[0], other[0]), min(rect[1], other[1]) + rect = [x, y, max(rect[0]+rect[2], other[0]+other[2])-x, + max(rect[1]+rect[3], other[1]+other[3])-y] + result = dict(bounds=rect, transit=transit) + fid = '' + if key in report['queue_positions']: + cx, cy = report['queue_positions'][key] + result = dict(bounds=[cx-transform.dim[0]/2, cy-height/2, transform.dim[0], height], transit=False) + elif key.startswith('guest/'): + target = person['target_facility_id'] + if person['current_service_id'] and person['guest_id'] in report['rooms'].get(target, {}).get('people', []): + fid = target + else: + sid = person['staff_id'] + if sid in report['carried_patients']: + result = copy.deepcopy(actor_region('staff/'+report['carried_patients'][sid])) + result['bounds'][1] -= 12*p + if 'center' in result: + result['center'][1] -= 12*p + elif sid in report['resting_staff']: + fid = report['resting_staff'][sid] + elif sid in report['treated_patients']: + fid = report['treated_patients'][sid] + else: + target = person['assigned_facility_id'] + cells = occupancy.get(target, []) + workers = report['rooms'].get(target, {}).get('staff', []) + if person['state'] in ('WORKING', 'AVAILABLE') and cell in cells and sid in workers: + rect = transform.room(cells) + columns = max(int(rect[2]/(16*p)), 1) + slot = workers.index(sid) + if rect[3] >= 40*p and slot < columns: + result = dict(bounds=[rect[0]+slot*rect[2]/columns, rect[1], rect[2]/columns, rect[3]], transit=False) + if fid and occupancy.get(fid): + result = dict(bounds=transform.room(occupancy[fid]), transit=False) + if key not in report['queue_positions'] and 'center' not in result and not result.get('no_lane'): + result = landing_actor_region(state, report, transform, cell, result['bounds'], result['transit']) + regions[key] = result + return result + for guest in runtime['guests']['guests']: + key, cell = 'guest/'+guest['guest_id'], guest['current_cell'] + if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']: + continue + center = actor_region(key).get('center', track_position(key, cell))[:] + if key in report['queue_positions']: + center = report['queue_positions'][key] + elif guest['current_service_id']: + fid = guest['target_facility_id'] + definition = facilities.get(fid, {}).get('definition_id', '') + cells = occupancy.get(fid, []) + if cells and cells[0][2] == report['layer']: + center = slot_position(fid, guest['guest_id'], definition, center, + 3*p-height*.5-(5*p if definition in ('standard_room', 'premium_room') else 0), True) + else: + center = offset(cell, center) + if center is not None: + expected.append((key, center, False, bool(guest['current_service_id']), False, False)) + for sid, member in staff.items(): + key, cell = 'staff/'+sid, member['current_cell'] + if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']: + continue + center = actor_region(key).get('center', track_position(key, cell))[:] + special = True + if key in report['queue_positions']: + special = False + center = report['queue_positions'][key] + elif sid in report['carried_patients']: + carrier = staff.get(report['carried_patients'][sid]) + if carrier: + center = track_position('staff/'+carrier['staff_id'], carrier['current_cell']) + center[1] -= 12*p + center = actor_region(key).get('center', center)[:] + elif sid in report['resting_staff']: + center = slot_position(report['resting_staff'][sid], sid, 'staff_waiting_room', center, -height*.5) + elif sid in report['treated_patients']: + center = slot_position(report['treated_patients'][sid], sid, 'infirmary', center, -2*p-height*.5) + else: + special = False + center = offset(cell, center) + fid = member['assigned_facility_id'] + cells = occupancy.get(fid, []) + workers = report['rooms'].get(fid, {}).get('staff', []) + if member['state'] in ('WORKING', 'AVAILABLE') and fid in facilities and cell in cells and sid in workers: + rect = transform.room(cells) + columns = max(int(rect[2]/(16*p)), 1) + index = workers.index(sid) + if rect[3] >= 40*p and index < columns: + center = [rect[0]+(index+.5)*rect[2]/columns, rect[1]+26*p-height*.5] + center = report['queue_positions'].get(key, center) + if center is not None: + expected.append((key, center, True, False, bool(member['carried_inventory_id']) and not special, False)) + floor_keys = [r[0] for r in expected] + cabin_bounds = {} + shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']} + for cabin in report['cabins']: + shaft = shafts[cabin['shaft_id']] + x,y,w,h = cabin['rect'] + columns = max(int((w/p-4)/8), 1) + passengers = shaft['boarded'] + for i, actor_id in enumerate(passengers): + key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1) + is_staff = key.startswith('staff/') + center = [x+(2+(i%columns+.5)*8)*p, y+h-(2+((i//columns)%2)*9)*p+3*p-height*.5] + carried = bool(staff.get(key.removeprefix('staff/'), {}).get('carried_inventory_id')) if is_staff else False + expected.append((key, center, is_staff, False, carried, True)) + cabin_bounds[key] = cabin['rect'] + members, stranded = [], [] + for key, center, is_staff, receiving, carrying, riding in expected: + person = people[key] + cell = person['current_cell'] + track = report['actor_tracks'].get(key, {}) + relocation = {} if riding else actor_region(key) + if riding: + region, transit = cabin_bounds[key], False + else: + region, transit = relocation['bounds'], relocation['transit'] + scale = .5 if riding else .75 + scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale + at = [center[0]-8*scale, center[1]+height*.5-3*p-17*scale] + rounded = [math.copysign(math.floor(abs(v)+.5), v) for v in at] + hashed = 5381 + for char in key: + hashed = (hashed*33+ord(char)) & 0xffffffff + variant = hashed % 15 + frame = int(report['animation_time']*12)+variant + activity = facilities.get(person.get('target_facility_id'), {}).get('definition_id', '') if not is_staff else 'work' + if is_staff and person['role_id'] == 'cleaner': + activity = 'idle' + task = next((t for t in runtime['staff']['tasks'] if t['task_id'] == person['current_task_id']), None) + room = facilities.get(person['assigned_facility_id'], {}) + if (person['state'] == 'WORKING' and person['health'] > 0 and not person['is_retiring'] + and room.get('definition_id') == 'cleaning_room' and room.get('state') in ('OPEN', 'DEGRADED') + and not room.get('suspension_reason') and task and task['task_type'] == 'facility_clean' + and task['state'] == 'WORKING' and task['assigned_staff_id'] == person['staff_id'] + and cell == task['target_cell'] and any(r['cell'] == cell for r in runtime['path']['cell_traffic']) + and not any(t['actor_id'] == 'staff:'+person['staff_id'] for t in runtime['elevators']['trips']) + and task['target_entity_id'] in facilities + and any(r['id'] == task['target_entity_id'] for r in runtime['facility_cleanliness']['facilities'])): + activity = 'clean' + moving = (not receiving and key not in report['queue_positions'] and not riding + and not report['paused'] and track.get('cell') != track.get('from') + and report['animation_time']-track.get('at', 0) < .42) + mode = 'idle' + if is_staff and person['state'] == 'INCAPACITATED': + mode = 'collapsed' + elif receiving and activity in ('standard_room', 'premium_room'): + mode = 'standard_room_sleep' if activity == 'standard_room' else 'sleep' + elif moving: + mode = 'carry_walk' if carrying else 'walk' + elif carrying: + mode = 'carry' + elif is_staff and person['state'] == 'WORKING' and not riding: + mode = activity if activity in ('clean', 'idle', 'work') else 'work' + elif receiving and activity in ('cafeteria', 'tavern'): + mode = 'eat' + elif key in report['queue_positions'] or (not is_staff and person['state'] == 'QUEUING'): + mode = 'wait' + states = enum_names(ROOT, 'scripts/staff/staff_runtime.gd' if is_staff else 'scripts/guest/guest_runtime.gd', 'State') + row = dict(key=key, center=center, at=rounded, scale=scale, cell=cell, mode=mode, + frame=frame, variant=variant, flip=track.get('direction', 1) < 0, + staff=is_staff, state=states.index(person['state']), receiving=receiving, + carrying=carrying, riding=riding) + if relocation.get('no_lane'): + aggregate = next((a for a in stranded if a['at'] == relocation['badge_at']), None) + if aggregate is None: + aggregate = dict(members=[], drawn_keys=[], at=relocation['badge_at'], text='') + stranded.append(aggregate) + aggregate['members'].append(dict(key=key, id=key.split('/', 1)[1], state=row['state'], cell=cell, mode=mode)) + aggregate['text'] = '+'+str(len(aggregate['members'])) + else: + members.append(dict(row=row, bounds=[region[0]+p, region[1]+p, region[2]-2*p, region[3]-2*p], transit=transit)) + fitted, aggregations = fitted_actor_layout(members, p, height) + aggregations = stranded + aggregations + close(report.get('actor_aggregations'), aggregations, 'actor aggregation/no-fit') + require([r['key'] for r in report['actor_drawings']] == [k for k in floor_keys if k in fitted], 'actor drawing coverage/order') + actual = list(report['actor_drawings']) + [r for c in report['cabins'] for r in c['passengers']] + require(len(actual) == len(fitted) and {r['key'] for r in actual} == set(fitted), 'missing or duplicate actor') + for row in actual: + require('region' not in row and 'boarded' not in row, 'unsupported actor projection flags') + for field, value in fitted[row['key']].items(): + close(row[field], value, 'actor fitted '+field) + exemptions = {m['row']['key'] for m in members if m['transit']} + validate_actor_nonoverlap(report, transit_keys=exemptions) + return ['actor_geometry', 'passenger_geometry'] + + +def compare_layouts(a, b): + require(a['declaration'] == b['declaration'], 'identical predeclared inputs') + declaration = a['declaration'] + require(declaration['metric'] == 'sum_destination_arrival_minutes_entire_cohort', 'predeclared metric') + require(a['initial'] == b['initial'], 'identical seed budget clock definitions and initial state') + arrivals = {r['id']: r['offset'] for r in declaration['arrivals']} + require(len(arrivals) == len(declaration['arrivals']) and arrivals, 'whole nonempty cohort') + builds = [] + metrics = [] + for records in (a, b): + commands = [records[k] for k in sorted(records) if k.startswith('build-')] + require(all(r['result']['code'] == 'OK' for r in commands), 'paid construction succeeded') + builds.append([r['payload'] for r in commands]) + require({k.removeprefix('arrival-') for k in records if k.startswith('arrival-')} == set(arrivals), 'arrival cohort identity') + for gid, offset in arrivals.items(): + row = records['arrival-' + gid] + require(row['offset'] == offset and row['result']['code'] == 'OK', 'actual accepted arrival') + require(row['payload'] == a['arrival-' + gid]['payload'], 'same actual arrival command') + completed = {} + first_minute = records['tick-000']['minute'] + for offset in range(declaration['window'] + 1): + tick = records['tick-%03d' % offset] + require(tick['minute'] == first_minute + offset, 'contiguous actual clock') + for guest in tick['guests']['guests']: + gid = guest['guest_id'] + require(gid in arrivals and offset >= arrivals[gid], 'actual cohort membership') + if guest['current_cell'] == declaration['to']: + completed.setdefault(gid, offset - arrivals[gid]) + require(set(completed) == set(arrivals), 'no unfinished or omitted journeys') + metrics.append(sum(completed.values())) + require(builds[1] == builds[0] + [declaration['improvement']], 'only predeclared layout improvement') + require(b['tick-000']['minute'] == a['tick-000']['minute'], 'same observation start') + require(metrics[1] < metrics[0], 'strict whole-cohort improvement') + return dict(metric=declaration['metric'], A=metrics[0], B=metrics[1]) + + +def drawing_context(stream, capture, report): + """Shared OS/offline binding: a missing stream never grants exemptions.""" + require(isinstance(capture, dict) and all(k in capture for k in ('report', 'state', 'rest_audit')), + 'missing raw drawing context') + close(read_object(stream, capture['report']), report, 'raw-state report binding') + state = read_object(stream, capture['state']) + rest = read_object(stream, capture['rest_audit']) + require(isinstance(state, dict) and isinstance(state.get('systems', {}).get('runtime'), dict), + 'malformed raw state context') + require(isinstance(rest, dict) and isinstance(rest.get('staff'), list) + and 'path_service_sha256' in rest, 'malformed rest authority') + return state, rest + + +def read_object(directory, digest): + require(isinstance(digest, str) and re.fullmatch('[0-9a-f]{64}', digest), 'invalid object digest') + path = directory / 'objects' / (digest + '.json') + require(not path.is_symlink() and sha(path) == digest, 'whole typed object hash') + typed = json.loads(path.read_bytes()) + require(typed.get('type') == 'Dictionary', 'whole dictionary required') + return decode(typed) + + +def inspect_stream(directory, root=ROOT): + labels = set() + checked = [] + with (directory / 'manifest.jsonl').open() as stream: + for line in stream: + row = json.loads(line) + if 'object' in row: + read_object(directory, row['object']) + continue + require(row['label'] not in labels, 'duplicate streamed label') + labels.add(row['label']) + require(type(row['pid']) is int and row['pid'] > 0, 'actual diagnostic PID required') + state = read_object(directory, row['state']) + close(read_object(directory, row['state_after']), state, 'read-only audit snapshot identity') + report = read_object(directory, row['report']) + fields = compare_actor_rows(state, report, root) + require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch') + validate_observation_labels(report) + compare_room_services(state, report, root) + compare_rest(state, report, read_object(directory, row['rest_audit']), root) + compare_queues(state, report) + compare_traffic(state, report) + compare_goods(state, report, root) + compare_badges(state, report) + compare_cabin_geometry(state, report) + compare_motion(state, report, read_object(directory, row['motion'])) + compare_actor_geometry(state, report) + checked.append(dict(label=row['label'], fields=fields + ['elevators', 'room_service_fields', + 'queues', 'traffic', 'goods', 'cabin_geometry'])) + require(checked, 'empty diagnostic stream') + return checked + + +def condition_matrix(executed=None): + executed = executed or {} + result = [] + for condition in CONDITIONS: + categories = {} + for category in CATEGORIES: + actual = set(executed.get(condition, {}).get(category, [])) + required = REQUIRED_CHECKS[condition][category] + missing = sorted(required - actual) + categories[category] = dict(checks=sorted(actual), missing=missing, + status='passed' if actual and not missing else 'pending') + result.append(dict(condition=condition, categories=categories, + status='passed' if all(c['status'] == 'passed' for c in categories.values()) else 'pending')) + return result + + +def BLOCKERS(executed=None): + return {r['condition']: {c: d['missing'] for c, d in r['categories'].items() if d['missing']} + for r in condition_matrix(executed) if r['status'] != 'passed'} + + +CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' +PLAN = 'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md' +BASE_COMMIT = '860136f7099f5e3aee99da5798a627a77f2d8eea' +ALLOWED_PATHS = ('scenes/main/world_preview.gd', 'scripts/ui/room_presentation.gd', + 'tests/ui/test_p23_life_observation.gd', 'tests/ui/test_p23_life_observation.gd.uid', + 'tests/fixtures/p23-03/**', 'tools/run_p23_life_observation.py', + 'tools/check_p23_life_observation_evidence.py', 'tools/test_p23_life_observation_evidence.py', + 'tools/regression/suite-policy.json', + 'docs/plans/original-completion-2026-10-02/execution/p23-03/**', CONTRACT) + + +def strict_json(data): + def pairs(items): + result = {} + for key, value in items: + require(key not in result, 'duplicate JSON key: ' + key) + result[key] = value + return result + return json.loads(data, object_pairs_hook=pairs, + parse_constant=lambda value: require(False, 'nonfinite JSON: ' + value)) + + +def marker_join(base, submitted, main): + """Preserve M verbatim structurally, adding exactly the S P23 row.""" + test = 'tests/ui/test_p23_life_observation.gd' + for document in (base, submitted, main): + rows = document['rows'] + require(len({row['test'] for row in rows}) == len(rows), 'duplicate marker test') + additions = [row for row in submitted['rows'] if row['test'] == test] + require(len(additions) == 1 and not any(row['test'] == test for row in base['rows'] + main['rows']), + 'marker must add exactly one new P23 row') + stripped = copy.deepcopy(submitted) + stripped['rows'] = [row for row in stripped['rows'] if row['test'] != test] + require(structural_equal(stripped, base), 'submitted marker changes non-P23 structure') + joined = copy.deepcopy(main) + joined['rows'].append(copy.deepcopy(additions[0])) + return joined + + +def structural_equal(a, b): + return json.dumps(a, sort_keys=True, ensure_ascii=False, allow_nan=False) == json.dumps( + b, sort_keys=True, ensure_ascii=False, allow_nan=False) + + +def verify_marker_join(base, submitted, main, tested): + expected = marker_join(base, submitted, main) + p23 = expected['rows'][-1] + require(structural_equal([row for row in tested['rows'] if row['test'] == p23['test']], [p23]), + 'joined marker P23 row changed or duplicated') + # merge-tree determines the insertion position; deleting the one addition + # must leave M's entire ordered structure, including all P18 rows, intact. + stripped = copy.deepcopy(tested) + stripped['rows'] = [row for row in stripped['rows'] if row['test'] != p23['test']] + require(structural_equal(stripped, main), 'completion marker is not the strict structural join') + + +def git_bytes(root, *arguments): + """Root-only adapter. Worker tests inject an in-memory object database.""" + process = subprocess.run(['git', '-C', str(root), *arguments], capture_output=True, check=False) + require(process.returncode == 0, 'Git composition failed/conflicted: ' + ' '.join(arguments)) + return process.stdout + + +_VERIFIED_VIEWS = {} + + +def blob_oid(content): + return hashlib.sha1(b'blob %d\0' % len(content) + content).hexdigest() + + +def git_blob_stream(root, oids, read_batch=None): + """Pace requests to one owned child; adapters return (binary stream, status).""" + requested = list(oids) + require(all(isinstance(oid, str) and re.fullmatch('[0-9a-f]{40}', oid) + for oid in requested), 'invalid blob oid') + process = stream = None + try: + if read_batch is None: + process = subprocess.Popen(['git', '-C', str(root), 'cat-file', '--batch'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + stream = process.stdout + else: + stream, status = read_batch(requested) + count = 0 + for oid in requested: + if process is not None: + process.stdin.write((oid + '\n').encode()) + process.stdin.flush() + header = stream.readline(128) + match = re.fullmatch(rb'([0-9a-f]{40}) blob (0|[1-9][0-9]*)\n', header) + require(match is not None and match[1].decode() == oid, 'invalid/out-of-order blob header') + size = int(match[2]) + content = stream.read(size) + require(len(content) == size, 'short blob read') + require(stream.read(1) == b'\n', 'missing blob LF') + require(blob_oid(content) == oid, 'blob content SHA-1 mismatch') + count += 1 + yield oid, content + if process is not None: + process.stdin.close() + require(stream.read(1) == b'', 'extra trailing blob output') + require((process.wait() if process is not None else status) == 0, 'cat-file failed') + require(count == len(requested), 'blob output count mismatch') + finally: + if process is not None: + if process.poll() is None: + process.kill() + process.wait() + process.stdin.close() + process.stdout.close() + elif stream is not None: + stream.close() + + +def verify_composition(root, roles, source_map, inputs, read_git=None, view=None, read_batch=None): + require(isinstance(roles, dict), 'missing externally bound S/M/T/Q') + names = ('submitted_sha', 'main_context_sha', 'tested_tree', 'tested_commit') + require(all(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]) for k in names), + 'invalid externally bound S/M/T/Q') + s, m, t, q = (roles[k] for k in names) + git = read_git or (lambda *args: git_bytes(root, *args)) + for commit in (BASE_COMMIT, s, m, q): + require(git('cat-file', '-t', commit).strip() == b'commit', 'role is not a commit') + require(git('cat-file', '-t', t).strip() == b'tree', 'T is not a tree') + require(git('rev-parse', q+'^{tree}').decode().strip() == t, 'Q does not contain T') + head = git('rev-parse', 'HEAD').decode().strip() + if head not in (s, q): + require(git('rev-parse', 'HEAD^{tree}').decode().strip() == t, 'C tree differs from T') + require(git('rev-list', '--parents', '-n', '1', 'HEAD').decode().split() == [head, m, s], + 'C parents must be exactly [M,S]') + require(not git('status', '--porcelain'), 'tested checkout is dirty') + merged = git('merge-tree', '--write-tree', s, m).decode().splitlines() + require(merged and merged[0] == t, 'T differs from deterministic merge-tree') + def tree(revision): + result = {} + for record in git('ls-tree', '-r', '-z', revision).split(b'\0'): + if not record: + continue + metadata, path = record.split(b'\t', 1) + mode, kind, oid = metadata.decode().split() + require(path.decode() not in result, 'duplicate T path') + result[path.decode()] = (mode, kind, oid) + return result + base, submitted, main, tested = map(tree, (BASE_COMMIT, s, m, t)) + changed_s = {p for p in base.keys() | submitted.keys() if base.get(p) != submitted.get(p)} + changed_m = {p for p in base.keys() | main.keys() if base.get(p) != main.get(p)} + def allowed(path): + return any(path.startswith(rule[:-2]) if rule.endswith('/**') else path == rule + for rule in ALLOWED_PATHS) + require(all(allowed(p) for p in changed_s), 'S changed a path outside allowed_paths') + for paths, authority, label in ((changed_s-changed_m, submitted, 'S-only'), + (changed_m-changed_s, main, 'M-only')): + for path in paths: + require(tested.get(path) == authority.get(path), label + ' blob/mode provenance: ' + path) + def document(revision): + return strict_json(git('show', revision + ':' + CONTRACT)) + verify_marker_join(document(BASE_COMMIT), document(s), document(m), document(t)) + runner = module('p23_composition_source_filter', ROOT/'tools/run_all_tests.py') + if view is not None: + registered = _VERIFIED_VIEWS.get(Path(view).resolve()) + require(registered is not None, 'unregistered verified view') + require(registered[0] == t, 'verified view tree mismatch') + require(registered[1] == {p: e for p, e in tested.items() if e[1] == 'blob'}, + 'verified view manifest mismatch') + with (contextlib.nullcontext(Path(view)) if view is not None else + tested_checkout(root, roles, git, read_batch)) as source_view: + require(source_map == {p: row['sha256'] for p, row in runner.source_refs(source_view).items()}, + 'incomplete tested-tree source/settings/QA map') + # Source refs include all QA suites and settings, not just S-changed files. + required = set(source_map) | {p.split('#', 1)[0] for p in inputs} | {PLAN, CONTRACT, + 'project.godot', 'tests/business/test_required_kitchen.gd', 'tools/regression/suite-policy.json'} + bindings = roles.get('blob_bindings') + require(isinstance(bindings, dict) and set(bindings) == required, 'incomplete/extraneous T blob bindings') + paths = sorted(required) + for path in paths: + entry = tested.get(path) + require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path) + contents = (((tested[p][2], (Path(view)/p).read_bytes()) for p in paths) if view is not None + else git_blob_stream(root, [tested[p][2] for p in paths], read_batch)) + with contextlib.closing(contents): + for number, (oid, content) in enumerate(contents): + path = paths[number] + require(blob_oid(content) == tested[path][2] == oid, 'binding blob SHA-1 mismatch: ' + path) + if head != s or path in changed_s-changed_m: + require((root/path).read_bytes() == content, 'checkout bytes differ from T: ' + path) + digest = hashlib.sha256(content).hexdigest() + if path in source_map: + require(source_map[path] == digest, 'evidence source is not T: ' + path) + if path in inputs: + require(inputs[path] == digest, 'evidence input is not T: ' + path) + return {k: roles[k] for k in names} + + +@contextlib.contextmanager +def tested_checkout(root, roles, read_git=None, read_batch=None): + """Disposable Git-blob view shared by the existing filters and readers.""" + require(isinstance(roles, dict), 'missing externally bound S/M/T/Q') + tree = roles.get('tested_tree', '') + require(isinstance(tree, str) and re.fullmatch('[0-9a-f]{40}', tree), 'invalid T') + git = read_git or (lambda *args: git_bytes(root, *args)) + with tempfile.TemporaryDirectory(prefix='p23-tested-tree-') as temporary: + view = Path(temporary) + manifest = {} + for record in git('ls-tree', '-r', '-z', tree).split(b'\0'): + if not record: + continue + metadata, raw_path = record.split(b'\t', 1) + mode, kind, oid = metadata.decode().split() + path = Path(raw_path.decode()) + require(not path.is_absolute() and '..' not in path.parts, 'unsafe T path') + require(kind == 'blob' and mode in ('100644', '100755'), 'T source aliases refused') + require(path.as_posix() not in manifest, 'duplicate T path') + manifest[path.as_posix()] = (mode, kind, oid) + paths = list(manifest) + contents = git_blob_stream(root, [manifest[p][2] for p in paths], read_batch) + with contextlib.closing(contents): + for number, (_, content) in enumerate(contents): + target = view/paths[number] + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content) + _VERIFIED_VIEWS[view.resolve()] = (tree, manifest) + try: + yield view + finally: + del _VERIFIED_VIEWS[view.resolve()] + + +def root_context(directory, index, explicit=None): + path = directory/'root-composition.json' + require(path.is_file() and not path.is_symlink() + and path.resolve().is_relative_to(directory.resolve()), 'missing/unsafe Root composition context') + data = path.read_bytes() + require(index.get('root_composition_sha256') == hashlib.sha256(data).hexdigest(), + 'Root composition context SHA mismatch') + roles = strict_json(data) + require(explicit is None or structural_equal(explicit, roles), 'explicit Root context disagrees') + return roles + + +def normative_plan_text(root): + """Requirement text is an input; explicitly labelled progress is metadata.""" + text = (root / PLAN).read_text() + section = re.search(r'^### P23[^\n]*\n(.*?)(?=^### |\Z)', text, re.M | re.S) + require(section is not None, 'missing normative #p23 plan section') + lines = [] + for line in section.group(0).splitlines(): + if line.startswith('現在地:'): + continue + line = line.split(' 状態:', 1)[0].rstrip() + if line: + lines.append(line) + require(any(line.startswith('- P23-03 ') and '受入:' in line for line in lines), + 'missing P23-03 work/acceptance plan text') + return '\n'.join(lines) + '\n' + + +def input_map(root): + names = [SPEC, CONTRACT, 'AGENTS.md', 'docs/traceability/original/BASELINE.txt'] + names += [p.relative_to(root).as_posix() for p in (root / 'tests/fixtures/p23-03').rglob('*') + if p.is_file()] + pins = {name: sha(root / name) for name in sorted(names)} + pins[PLAN + '#p23-normative'] = hashlib.sha256(normative_plan_text(root).encode()).hexdigest() + return pins + + +def input_provenance(root): + # Full-plan progress changes deliberately do not participate in input equality. + return {'full_plan_sha256': sha(root / PLAN)} + + +def inspect_os_receipt(receipt, source_map, pins=None): + require(receipt.get('engine_ended') is True, 'engine_ended must be true') + require(receipt.get('passed') is True and receipt.get('exit_code') == 0, 'unsuccessful OS process') + require(not receipt.get('error') and receipt.get('guard_errors') == [] + and receipt.get('cleanup_errors') == [], 'OS cleanup/guard failure') + require(receipt.get('default_before') == receipt.get('default_after') + and isinstance(receipt.get('default_before'), dict), 'default profile changed') + require(receipt.get('stage_before') == receipt.get('stage_after') + and isinstance(receipt.get('stage_before'), dict), 'stage changed') + require(receipt.get('source_map') == source_map, 'OS source pin differs') + after = receipt.get('source_after', {}) + require({name: pin['sha256'] for name, pin in after.items()} == source_map, 'source changed') + cleanup = receipt.get('owned_cleanup') + require(isinstance(cleanup, list) and cleanup + and all(row.get('engine_ended') is True for row in cleanup), 'missing owned cleanup') + require(bool(receipt.get('ordinary_owner_proof')), 'missing ordinary audio owner proof') + for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'): + require(isinstance(receipt.get(field), str) and re.fullmatch('[0-9a-f]{64}', receipt[field]) + and receipt[field] == receipt.get(field + '_after'), 'OS pin changed: ' + field) + require(receipt.get('spec_sha256') == SPEC_SHA, 'OS immutable spec') + require(receipt.get('inputs') and receipt['inputs'] == receipt.get('inputs_after'), 'OS inputs changed') + if pins is not None: + require(receipt['inputs'] == pins, 'OS input applicability') + require(receipt.get('environment') and receipt['environment'] == receipt.get('environment_after'), 'OS environment changed') + require(type(receipt.get('actual_pid')) is int and receipt['actual_pid'] > 0, 'OS actual PID') + require(receipt.get('profile') and receipt.get('profile_name') and isinstance(receipt.get('profile_after'), dict), 'OS private profile provenance') + before = receipt.get('profile_before', {}) + require(type(before.get('existed')) is bool and isinstance(before.get('files'), dict), 'OS profile before') + for member, pin in before['files'].items(): + require(receipt['profile_after'].get(member) == pin, 'OS profile input changed: ' + member) + + +def inspect_resume(writer, reader, saved, requests, formal_members): + require(type(writer['actual_pid']) is int and type(reader['actual_pid']) is int + and writer['actual_pid'] > 0 and reader['actual_pid'] > 0 + and writer['actual_pid'] != reader['actual_pid'], 'resume requires distinct actual PIDs') + require(saved['pid'] == writer['actual_pid'], 'formal writer PID') + require(set(formal_members) == {'world.bin', 'manifest.json', 'thumbnail.png', 'journal.log'}, 'formal members coverage') + close(saved['members'], formal_members, 'formal member hashes') + require({k: v['sha256'] for k, v in reader['formal_input'].items()} == formal_members, 'reader formal input hashes') + loads = [row for row in requests if row['request']['action'] == 'load_menu'] + require(len(loads) == 1, 'missing or duplicate normal load ACK') + item = loads[0] + request, ack, observed, cua = [item[k] for k in ('request', 'ack', 'observed', 'cua')] + require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == reader['actual_pid'], 'load ACK PID') + require(request['sequence'] == ack['sequence'] == observed['sequence'], 'load ACK request ID') + require(ack['request_sha256'] == item['request_sha256'] == observed['request_sha256'], 'load ACK hash') + require(0 < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'load ACK time order') + require(cua['method'] == 'native_os' and cua['tool_call_id'] and cua['timestamp_ns'] > 0, 'load CUA provenance') + require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'load external ACK time order') + require(observed['status'] == '手動セーブを読み込みました。', 'normal load result') + return ['distinct_pid', 'formal_members', 'load_ack'] + + +def require_screenshot_coverage(action, screenshots): + require(isinstance(screenshots, list) and len(screenshots) >= (1 if action == 'screenshot' else 2), + 'screenshot-only needs one image; real input needs before/after images') + + +def inspect_os_requests(directory, layout, role, pid, root=ROOT): + pair = directory/'os-pair'/layout + def path(name): + result = directory/name + require(result.resolve().is_relative_to(directory.resolve()) and not result.is_symlink(), 'OS artifact escape') + return result + def document(name): + return json.loads(path(name).read_bytes()) + listing = json.loads((pair/(role+'-requests.json')).read_bytes()) + require(listing['pid'] == pid and listing['requests'] and len(set(listing['requests'])) == len(listing['requests']), 'OS request list') + bindings = json.loads((pair/(role+'-screenshots.json')).read_bytes()) + seen, records, pixel_categories = set(), [], set() + stream = pair/(role+'-stream') + captures = {row['label']: row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines()) if 'report' in row} + prior_tick = -1 + for sequence, ack_name in enumerate(listing['requests'], 1): + ack_path = path(ack_name) + request_path = ack_path.with_name('request.json') + observed = json.loads(ack_path.with_name('observed.json').read_bytes()) + request, ack = json.loads(request_path.read_bytes()), json.loads(ack_path.read_bytes()) + cua = document(ack['input_receipt']) + require_screenshot_coverage(request['action'], cua.get('screenshots')) + require(request['sequence'] == ack['sequence'] == observed['sequence'] == sequence, 'OS request sequence') + require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == pid, 'OS request PID') + digest = sha(request_path) + require(ack['request_sha256'] == observed['request_sha256'] == digest, 'OS ACK hash') + require(prior_tick < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'OS ACK order') + prior_tick = observed['ack_observed_ticks_usec'] + require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'OS external ACK time order') + require(cua['method'] == 'native_os' and cua['tool_name'] and cua['tool_call_id'] + and cua['timestamp_ns'] > 0 and cua['collector_pid'] > 0 and cua['collector_pid'] != pid, 'OS CUA provenance') + require('arguments' in cua and isinstance(cua['arguments'], dict), 'full CUA arguments required') + if request['action'] != 'screenshot': + require(cua['events'] and observed['events'], 'OS input events missing') + if request['action'] == 'wheel': + require(request['ctrl'] is False and any(e.get('ctrl') is False and e.get('pressed') for e in observed['events']), 'ordinary wheel evidence') + report = decode(request['report']) + capture = captures[request_path.parent.name] + state, rest_audit = drawing_context(stream, capture, report) + for number, shot in enumerate(cua['screenshots']): + matches = [b for b in bindings if b['request'] == str(request_path.relative_to(directory)) and b['screenshot_index'] == number] + require(len(matches) == 1, 'screenshot binding coverage') + binding = matches[0] + identity = (binding['request'], number) + require(identity not in seen, 'duplicate screenshot binding') + seen.add(identity) + require(binding['request_sha256'] == digest and binding['target_pid'] == pid and binding['sequence'] == sequence, + 'screenshot request attribution') + require(binding['input_receipt'] == ack['input_receipt'] and binding['input_receipt_sha256'] == sha(path(ack['input_receipt'])), 'screenshot CUA receipt hash') + require(binding['raw_path'] == shot['path'] and binding['raw_sha256'] == shot['raw_sha256'] == sha(path(shot['path'])) + and binding['tool_call_id'] == shot['tool_call_id'] and shot['tool_call_id'], 'screenshot raw provenance') + conversion = document(binding['conversion']) if binding['conversion'] else None + image = screenshot_pixels(path(shot['path']), conversion, shot) + require(binding['png_sha256'] == sha(path(binding['png_path'])) and (binding['width'], binding['height']) == image[:2], 'decoded screenshot binding') + if conversion: + require(Path(conversion['png_path']).resolve() == path(binding['png_path']).resolve(), 'converted output binding') + for suffix in ('stdout', 'stderr'): + require(sha(path(binding['conversion']+'.'+suffix)) == conversion[suffix+'_sha256'], 'converter log hash') + else: + require(binding['png_path'] == binding['raw_path'], 'PNG must bind original bytes') + # Screenshot actions observe a stable report; input before/after + # screenshots are provenance only and never relabelled as this frame. + if request['action'] == 'screenshot': + clicks = [document(json.loads(path(name).read_bytes())['input_receipt']) + for name in listing['requests']] + frame = frame_rows(report, decode(request['rectangles']), request, cua, [image], clicks, root, + state, rest_audit) + review_dir = request_path.parent/('review-image-' + str(number)) + manifest, manifest_sha = write_frame_review(review_dir, frame, [dict( + whole_image_sha256=binding['png_sha256'], raw_sha256=shot['raw_sha256'], + conversion_sha256=sha(path(binding['conversion'])) if conversion else None, + request_sha256=digest, receipt_sha256=sha(path(ack['input_receipt'])))]) + require(frame['mechanical_pass'], 'frame mechanical checks: ' + str(frame['errors']) + + str([r['failures'] for r in frame['rows'] if not r['mechanical_pass']])) + pixel_categories.update(reviewed_pixel_categories( + manifest, manifest_sha, review_dir/'independent-verdict.json', require_judgments=True)) + records.append(dict(request=request, ack=ack, observed=observed, cua=cua, request_sha256=digest)) + require(len(seen) == len(bindings), 'unattributed screenshot bindings') + return records, pixel_categories + + +def inspect_native_phases(directory, index, root=ROOT): + runner = module('p23_existing_runner_checks', root/'tools/run_all_tests.py') + expected = ['runs/'+name+'/'+name+'-receipt.json' for name in ('import', 'parse', 'layout-a', 'layout-b')] + require(index['phases'] == expected, 'native phase coverage/order') + for name in expected: + path = directory/name + receipt = json.loads(path.read_bytes()) + pins = json.loads(path.with_name('p23-inputs.json').read_bytes()) + require(pins['receipt_sha256'] == sha(path), 'native receipt pin') + require(pins['source_map'] == index['source_map'] and {k: v['sha256'] for k, v in pins['source_after'].items()} == index['source_map'], 'native sources changed') + require(pins['inputs'] == pins['inputs_after'] == input_map(root) and pins['spec_sha256'] == SPEC_SHA, 'native input applicability') + require(pins['default_before'] == pins['default_after'] and pins['stage_before'] == pins['stage_after'], 'native profile/stage changed') + require(pins['settings_sha256'] == sha(root/'project.godot'), 'native settings changed') + require(receipt['passed'] is True and receipt['failed'] is False and receipt['engine_ended'] is True + and receipt['exit_code'] == 0 and not receipt.get('validation_error') and not receipt.get('guard_error'), 'native process failed') + capture = receipt['outer_actual_capture'] + require(capture['passed'] is True and capture['all_captured_ended'] is True, 'native owned cleanup') + require(receipt['whole_startup_profile_after']['fresh_before_engine'] is True, 'native profile protection') + require(receipt['default_player_entire_profile_unchanged'] is True, 'native default profile changed') + require(isinstance(receipt['controlled_environment'], dict), 'native environment absent') + diagnostics = receipt['finite_original_diagnostic_review'] + require(runner.process_gate(receipt['native_process'], diagnostics['allowed_e31_error_lines']), 'native process gate') + if receipt['test']: + require(receipt['source_sha256'] == receipt['source_sha256_end'] == sha(root/receipt['test']) + and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker') + + +def check(root, directory, root_roles=None): + try: + index_path = directory/'index.json' + if not index_path.is_file(): + return ['missing evidence index: ' + str(index_path)] + index = strict_json(index_path.read_bytes()) + roles = root_context(directory, index, root_roles) + with tested_checkout(root, roles) as view: + return check_tested(root, directory, roles, view) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + return ['invalid evidence index: ' + str(exc)] + + +def check_tested(checkout, directory, root_roles, root): + """Never infer completion of unimplemented gates from an index assertion.""" + deficits = [] + executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS} + semantic, pixel_categories, anchors = set(), set(), set() + index_path = directory / 'index.json' + if not index_path.is_file(): + return ['missing evidence index: ' + str(index_path)] + try: + index = json.loads(index_path.read_bytes()) + require(index.get('schema') == 'p23-life-observation-v2', + 'historical v1 evidence has diagnostic applicability only') + runner = module('p23_source_applicability', root/'tools/run_all_tests.py') + require(index.get('source_map') == {n: v['sha256'] for n, v in runner.source_refs(root).items()}, 'exact frozen source map') + roles = verify_composition(checkout, root_roles, index['source_map'], input_map(root), view=root) + if git_bytes(checkout, 'rev-parse', 'HEAD').decode().strip() != roles['submitted_sha']: + require(index['source_map'] == {n: v['sha256'] for n, v in runner.source_refs(checkout).items()}, + 'exact checkout source map') + require(index.get('composition') == roles and index.get('commit') == roles['tested_commit'], + 'evidence role binding differs from Root S/M/T/Q; historical v1 is not T evidence') + if index.get('input_map') != input_map(root): + deficits.append('normative input_map differs from current immutable inputs') + if index.get('spec_sha256') != SPEC_SHA: + deficits.append('immutable spec SHA missing or different') + if index.get('marker_contract_sha256') != sha(root / CONTRACT): + deficits.append('marker contract SHA missing or different') + artifacts = index.get('artifacts', {}) + if not artifacts: + deficits.append('missing indexed evidence artifacts') + for name, digest in artifacts.items(): + path = directory / name + require(path.resolve().is_relative_to(directory.resolve()) and not path.is_symlink(), + 'artifact escapes evidence directory') + require(path.is_file() and sha(path) == digest, 'artifact SHA mismatch: ' + name) + try: + inspect_native_phases(directory, index, root) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + deficits.append('native evidence deficit: ' + str(exc)) + for layout in ('A', 'B'): + layout_semantic, layout_pixels = set(), set() + stream = directory / ('layout-' + layout) + if (stream / 'manifest.jsonl').is_file(): + inspect_stream(stream, root) + else: + deficits.append('missing native layout-' + layout + ' typed diagnostic stream') + for role in ('writer', 'reader'): + stream = directory / 'os-pair' / layout / (role + '-stream') + if (stream / 'manifest.jsonl').is_file(): + inspect_stream(stream, root) + # Successful independent recalculation, with nonempty + # observed rows required before a category earns coverage. + for line in (stream/'manifest.jsonl').read_text().splitlines(): + capture = json.loads(line) + if 'report' not in capture: continue + report = read_object(stream, capture['report']) + presence = {'room': report['rooms'], 'queue': report['landing_badges'], + 'EV': report['cabins'], 'staff': report['staff'], + 'goods': report['goods']['requests'], 'business': report['badges']} + semantic.update('semantic_'+c for c, rows in presence.items() if rows) + layout_semantic.update(c for c, rows in presence.items() if rows) + if any(r['people'] for r in report['rooms'].values()): anchors.add('room_users') + if any(r['overflow'] > 0 for r in report['landing_badges']): anchors.add('queue_overflow') + if any(r['passengers'] for r in report['cabins']): anchors.add('elevator_passengers') + if report['staff'] and report['tasks']: anchors.add('staff_jobs') + if any(r['carried_inventory_id'] for r in report['staff']) and report['goods']['requests']: anchors.add('carried_goods') + if role == 'reader': anchors.add('resume_recalculation') + else: + deficits.append('missing OS ' + layout + '-' + role + ' typed diagnostic stream') + ended = directory / 'os-runs' / (layout + '-' + role) / 'ended.json' + if ended.is_file(): + receipt = json.loads(ended.read_bytes()) + inspect_os_receipt(receipt, index.get('source_map'), input_map(root)) + records, categories = inspect_os_requests(directory, layout, role, receipt['actual_pid'], root) + pixel_categories.update('pixels_'+c for c in categories if c in CATEGORIES) + layout_pixels.update(categories) + if role == 'reader': + writer = json.loads((directory/'os-runs'/(layout+'-writer')/'ended.json').read_bytes()) + pair = directory/'os-pair'/layout + members = {p.name: sha(p) for p in (pair/'formal').iterdir() if p.is_file()} + inspect_resume(writer, receipt, json.loads((pair/'save.json').read_bytes()), records, members) + require((pair/'writer-formal-model.var').read_bytes() == (pair/'reader-formal-model.var').read_bytes(), 'resumed whole formal model') + anchors.add('distinct_pid_resume') + else: + deficits.append('missing OS ' + layout + '-' + role + ' process receipt') + for category in CATEGORIES: + if category not in layout_semantic or category not in layout_pixels: + deficits.append('missing actual OS ' + layout + ' semantic/pixel coverage: ' + category) + if all((directory / ('layout-' + layout) / 'manifest.jsonl').is_file() for layout in ('A', 'B')): + compare_layouts(comparison_records(directory / 'layout-A'), comparison_records(directory / 'layout-B')) + anchors.add('route_improvement') + if not deficits: + anchors.add('os_guards') + for condition in CONDITIONS: + for category in CATEGORIES: + executed[condition][category].update((semantic | pixel_categories | anchors) & REQUIRED_CHECKS[condition][category]) + deficits.extend(condition + ': ' + json.dumps(missing, ensure_ascii=False) for condition, missing in BLOCKERS(executed).items()) + except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc: + deficits.append('invalid evidence index: ' + str(exc)) + return deficits + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--verify-only', action='store_true') + parser.add_argument('--project-root', type=Path, default=ROOT) + parser.add_argument('--diagnostics', type=Path) + parser.add_argument('--offline-mirror', type=Path) + parser.add_argument('--layout-a', type=Path) + parser.add_argument('--layout-b', type=Path) + parser.add_argument('--root-composition', type=Path) + args = parser.parse_args(argv) + check_setup_budget_source((args.project_root / 'tests/fixtures/p23-03/observation.gd').read_text()) + if args.offline_mirror: + require(not any((args.verify_only, args.diagnostics, args.layout_a, args.layout_b)), + 'offline preparation must not be combined with acceptance checks') + print(json.dumps(inspect_offline_mirror(args.offline_mirror), ensure_ascii=False)) + return 2 + if args.verify_only or (os.environ.get('BUILD_OUTPUT_DIR') and not any( + (args.diagnostics, args.layout_a, args.layout_b))): + build = os.environ.get('BUILD_OUTPUT_DIR') + if not build: + print('BUILD_OUTPUT_DIR is required', file=sys.stderr) + return 2 + try: + root_roles = strict_json(args.root_composition.read_bytes()) if args.root_composition else None + deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence', root_roles) + except (OSError, ValueError) as exc: + deficits = ['invalid Root composition context: ' + str(exc)] + print(json.dumps(dict(task_id='P23-03', integrity='fail' if deficits else 'pass', + deficits=deficits, acceptance='pending'), ensure_ascii=False)) + return 1 if deficits else 0 + # Registration hashes the immutable JSON after trimming outer whitespace. + require(hashlib.sha256((ROOT / SPEC).read_text().strip().encode()).hexdigest() == SPEC_SHA, + 'immutable spec mismatch') + checked = inspect_stream(args.diagnostics.resolve()) if args.diagnostics else [] + require(bool(args.layout_a) == bool(args.layout_b), 'both layout streams required') + comparison = compare_layouts(comparison_records(args.layout_a), comparison_records(args.layout_b)) if args.layout_a else None + print(json.dumps(dict(task_id='P23-03', status='blocked', semantic_diagnostics=checked, + layout_comparison=comparison, condition_matrix=condition_matrix(), root_evidence='pending'), ensure_ascii=False)) + return 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py new file mode 100644 index 0000000..0a25514 --- /dev/null +++ b/tools/run_p23_life_observation.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +"""Root-only native collection. Does not run regression or claim acceptance.""" +import argparse +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import time +import uuid +import re + +sys.dont_write_bytecode = True +HERE = Path(__file__).resolve().parent +CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json' +CONDITIONS = ('room_occupancy_and_operation', 'queues_and_overflow', + 'elevator_state_and_passengers', 'staff_state_and_jobs', + 'supplies_and_carried_goods', 'same_demand_route_improvement', + 'normal_os_and_distinct_pid_resume') +OS_CHILD_TIMEOUT_SECONDS = 3600 +RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd' + +def collect_screenshots(out, pair, role): + """Bind every external CUA screenshot to its request and decoded PNG. + + The external receipt supplies screenshots[{path,raw_sha256,tool_call_id}]. + Paths are relative to the evidence root; raw receipt bytes remain unchanged. + """ + checker = module('p23_screenshot_binding', HERE/'check_p23_life_observation_evidence.py') + bindings = [] + requests = json.loads((pair/(role+'-requests.json')).read_text())['requests'] + for ack_name in requests: + ack_path = out/ack_name + checker.require(ack_path.resolve().is_relative_to(out.resolve()), 'ACK path escape') + ack = json.loads(ack_path.read_text()) + request_path = ack_path.with_name('request.json') + request = json.loads(request_path.read_text()) + receipt_path = out/ack['input_receipt'] + checker.require(receipt_path.resolve().is_relative_to(out.resolve()), 'CUA receipt path escape') + cua = json.loads(receipt_path.read_text()) + checker.require(ack['request_sha256'] == checker.sha(request_path), 'CUA request hash') + checker.require(cua['screenshots'], 'CUA screenshots missing') + for number, shot in enumerate(cua['screenshots']): + raw = out/shot['path'] + checker.require(raw.resolve().is_relative_to(out.resolve()) and not raw.is_symlink(), 'screenshot path escape') + checker.require(checker.sha(raw) == shot['raw_sha256'] and shot['tool_call_id'], 'raw screenshot CUA binding') + kind, _, _ = checker.image_kind(raw.read_bytes()) + conversion = None + if kind == 'jpeg': + png = ack_path.with_name('decoded-%02d.png' % number) + receipt = ack_path.with_name('conversion-%02d.json' % number) + convert_screenshot(raw.resolve(), png.resolve(), shot, receipt.resolve()) + conversion = str(receipt.relative_to(out)) + else: + png = raw + width, height, _ = checker.png_pixels(png.read_bytes()) + bindings.append(dict(request=str(request_path.relative_to(out)), request_sha256=checker.sha(request_path), + sequence=request['sequence'], target_pid=request['target_pid'], + input_receipt=ack['input_receipt'], input_receipt_sha256=checker.sha(receipt_path), + screenshot_index=number, tool_call_id=shot['tool_call_id'], + raw_path=shot['path'], raw_sha256=shot['raw_sha256'], conversion=conversion, + png_path=str(png.relative_to(out)), png_sha256=checker.sha(png), width=width, height=height)) + checker.require(bindings, 'empty screenshot collection') + destination = pair/(role+'-screenshots.json') + with destination.open('x') as output: + json.dump(bindings, output, indent=2) + output.write('\n') + return str(destination.relative_to(out)) + +def convert_screenshot(raw_path, png_path, cua_receipt, receipt_path): + """Root-only conversion; raw CUA bytes are never renamed or rewritten.""" + checker = module('p23_conversion_checker', HERE/'check_p23_life_observation_evidence.py') + raw, png, receipt = map(Path, (raw_path, png_path, receipt_path)) + checker.require(raw.is_absolute() and png.is_absolute() and receipt.is_absolute(), + 'absolute conversion paths required') + checker.require(raw.is_file() and not raw.is_symlink() and not png.exists() + and not receipt.exists() and raw.resolve() != png.resolve(), 'fresh separate conversion output') + raw_hash = checker.sha(raw) + checker.require(raw_hash == cua_receipt['raw_sha256'] and cua_receipt['tool_call_id'], + 'CUA conversion input hash/call mismatch') + kind, width, height = checker.image_kind(raw.read_bytes()) + checker.require(kind == 'jpeg', 'conversion requires JPEG input') + converter = '/usr/bin/sips' + converter_hash = checker.sha(converter) + argv = [converter, '-s', 'format', 'png', str(raw), '--out', str(png)] + process = subprocess.run(argv, capture_output=True, timeout=900, check=False) + rec = dict(raw_path=str(raw), raw_sha256=raw_hash, raw_kind=kind, + tool_call_id=cua_receipt['tool_call_id'], converter=converter, + converter_sha256=converter_hash, argv=argv, exit_code=process.returncode, + stdout_sha256=hashlib.sha256(process.stdout).hexdigest(), + stderr_sha256=hashlib.sha256(process.stderr).hexdigest(), + png_path=str(png), png_sha256=checker.sha(png) if png.is_file() else None, + width=width, height=height) + # Preserve failure receipts too. These logs allow independent hash checking. + with receipt.open('x') as output: + json.dump(rec, output, indent=2) + output.write('\n') + with Path(str(receipt) + '.stdout').open('xb') as output: + output.write(process.stdout) + with Path(str(receipt) + '.stderr').open('xb') as output: + output.write(process.stderr) + checker.require(checker.sha(raw) == raw_hash, 'raw CUA bytes changed during conversion') + checker.require(checker.sha(converter) == converter_hash, 'converter changed during conversion') + checker.inspect_conversion(rec, cua_receipt, converter_hash) + return rec + +def collect_os(r, root, out, stage, engine, commit, refs, protected, outer): + """Adapted P08-03 collect_os protocol, retaining owned-process guards. + + Root supplies CUA ack/receipt files in the requested directories. Neither + controller nor passive endpoints generate input or change permissions. + """ + r.require(not outer.native.helper['live_engines'](), 'external engine present') + checker = module('p23_os_pins', HERE/'check_p23_life_observation_evidence.py') + before = r.inventory(stage, stage=True) + public = (stage/'project.godot').read_bytes() + (out/'os-pair').mkdir() + phases = [] + for label in ('A-writer', 'A-reader', 'B-writer', 'B-reader'): + layout, role = label.split('-') + pair = out/'os-pair'/layout + pair.mkdir(exist_ok=role == 'reader') + r.require(not outer.native.helper['live_engines'](), 'external engine present') + r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards') + r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard') + r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission') + folder = out/'os-runs'/label + folder.mkdir(parents=True) + name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p23-observation-'+label.lower() + profile = r.profile_path(name) + r.require(not profile.exists(), 'fresh OS private profile') + formal_source = pair/'formal' if role == 'reader' else None + formal_before = r.inventory(formal_source) if formal_source else {} + if role == 'reader': + r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs') + formal = profile/'saves/manual_ui' + formal.mkdir(parents=True) + for member, pin in formal_before.items(): + r.copy_source(formal_source/member, formal/member) + r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA') + recorder = folder/'passive_os.gd' + r.copy_source(root/RECORDER, recorder) + proof = folder/'teardown' + (proof/'owned-audio').mkdir(parents=True) + env = r.fresh_environment(dict(os.environ)) + env.update(P23_OBSERVATION_ROOT=str(pair), P23_OBSERVATION_ROLE=role, P23_LAYOUT=layout, + THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'), + THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json')) + command = [engine, '--path', str(stage), '--script', str(recorder)] + base = {'protocol':'p23-observation-native-os-v1', 'role':role, 'layout':layout, 'commit':commit, + 'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage), + 'stage_before':before, 'profile':str(profile), 'profile_name':name, + 'profile_before':{'existed':profile.exists(), 'files':r.inventory(profile) if profile.exists() else {}}, 'formal_input':formal_before, + 'formal_source':str(formal_source) if formal_source else None, 'owner_pid':os.getpid(), + 'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine), + 'controller_sha256':r.sha(root/'tools/run_p23_life_observation.py'), + 'recorder_sha256':r.sha(recorder), 'formal_acceptance':False, + 'timeout_seconds':OS_CHILD_TIMEOUT_SECONDS, + 'inputs':checker.input_map(root), 'spec_sha256':checker.SPEC_SHA, + 'settings_sha256':hashlib.sha256(public).hexdigest(), + 'environment':{k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))}, + 'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()} + r.write(folder/'started.json', base) + error = None; children = []; guards = []; exit_code = None; owner_proof = None + try: + (stage/'project.godot').write_bytes(r.private_project(public, name)) + with (folder/'engine-native.log').open('x') as log: + process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT) + children.append(process) + r.write(folder/'process-started.json', dict(base, actual_pid=process.pid, + monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests')) + print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair), + 'state':'waiting_for_root_cua', 'role':role}), flush=True) + exit_code = process.wait(timeout=base['timeout_seconds']) + r.require(exit_code == 0, 'ordinary native OS exit0') + raw = (folder/'engine-native.log').read_text() + r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics') + matches = re.findall(r'(?m)^P23 passive OS observation: ([0-9]+) checks, 0 failures$', raw) + r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion') + observed = r.read(pair/(role+'-profile.json')) + r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve() + and observed['display'] != 'headless', 'actual OS private profile/display') + collect_screenshots(out, pair, role) + owners = r.module('p23_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py') + owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single']) + except BaseException as caught: + error = type(caught).__name__+': '+str(caught) + finally: + try: + cleanup, cleanup_errors = outer.finish_captured(children) + finally: + (stage/'project.godot').write_bytes(public) + try: + r.require(r.source_refs(root) == refs, 'OS source unchanged') + r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged') + r.require(r.default_player(root) == protected, 'OS default player unchanged') + if formal_source: + r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged') + r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged') + except BaseException as caught: + guards.append(type(caught).__name__+': '+str(caught)) + passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0 + r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None, + exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup), + error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup, + ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True), + engine_sha256_after=r.sha(engine), inputs_after=checker.input_map(root), + settings_sha256_after=hashlib.sha256((stage/'project.godot').read_bytes()).hexdigest(), + environment_after={k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))}, + environment_sha256_after=hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest(), + default_after=r.default_player(root), source_after=r.source_refs(root), + profile_after=r.inventory(profile), passed=passed)) + phases.append('os-runs/'+label+'/ended.json') + r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors)) + return phases + +def module(name, path): + spec = importlib.util.spec_from_file_location(name, path) + obj = importlib.util.module_from_spec(spec) + spec.loader.exec_module(obj) + return obj + +def absolute(value): + path = Path(value) + if not path.is_absolute(): + raise argparse.ArgumentTypeError('absolute path required') + return path.resolve() + +def collect(args): + root, out = args.project_root, args.output + if out.is_relative_to(root): + raise ValueError('evidence output must be outside the frozen source checkout') + r = module('p23_standard_runner', root/'tools/run_all_tests.py') + commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() + r.require(commit == args.commit, 'actual tested commit Q required') + r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required') + r.require(not out.exists(), 'fresh output; never overwrite earlier proof') + refs = r.source_refs(root) + checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py') + inputs_before = checker.input_map(root) + root_roles = checker.strict_json(args.root_composition.read_bytes()) + composition = checker.verify_composition(root, root_roles, + {name: pin['sha256'] for name, pin in refs.items()}, inputs_before) + outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py') + r.require(not outer.native.helper['live_engines'](), 'one native engine') + engine = shutil.which(args.godot) + r.require(engine is not None, 'native engine exists') + engine = str(Path(engine).resolve()) + out.mkdir(parents=True) + stage = out/'stage'; stage.mkdir() + (out/'starters').mkdir(); (out/'bootstrap').mkdir() + for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items(): + data = pin['text'].encode() + r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin') + (out/'bootstrap'/name).write_bytes(data) + for name, pin in refs.items(): + target = stage/name; target.parent.mkdir(parents=True, exist_ok=True) + r.copy_source(Path(pin['path']), target) + r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name) + expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()} + selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180} + r.require(r.inventory(stage, stage=True) == expected, 'complete private source') + protected = r.default_player(root); public = (stage/'project.godot').read_bytes() + paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')] + r.write(out/'parse-paths.json', paths) + markers = r.module('p23_strict_markers', root/'tools/strict_test_markers.py') + contracts = markers.load_contracts(root/CONTRACT) + policy = r.read(r.SUPPORT/'suite-policy.json') + diagnostics = r.module('p23_diagnostics', r.SUPPORT/'diagnostic_gate.py') + owners = r.module('p23_owners', r.SUPPORT/'ordinary_proof_gate.py') + audio = r.module('p23_audio', r.SUPPORT/'audio_evidence_gate.py') + r.write(out/'runner-input.json', {'protocol':'p23-normal-observation-v1', 'commit':commit, + 'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine), + 'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':806042, + 'inputs':inputs_before, 'provenance':checker.input_provenance(root), + 'pixel_decoder':{'interpreter':sys.executable, + 'PIL_available':importlib.util.find_spec('PIL') is not None, + 'raw_screenshots':'preserve bytes; Root lossless conversion required for JPEG without PIL'}}) + phases = []; previous = {}; cases = []; error = None; os_proof = None + deadline = time.monotonic()+900 + saved_env = {k:os.environ.get(k) for k in ('P23_LAYOUT','P23_OUTPUT')} + def phase(label, test): + remaining = int(deadline-time.monotonic()) + r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline') + selected['timeout'] = min(180, remaining) + phase_out = out/'runs'/label + phase_out.mkdir(parents=True) + (phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir() + for original in (out/'bootstrap').iterdir(): + r.copy_source(original,phase_out/'bootstrap'/original.name) + r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy') + r.write(phase_out/'parse-paths.json',paths) + row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous, + policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio) + # Supplement the existing runner receipt; preserve it verbatim. + r.write(phase_out/'p23-inputs.json', dict(source_map=selected['map'], source_after=r.source_refs(root), + engine_sha256=r.sha(engine), inputs=inputs_before, inputs_after=checker.input_map(root), + spec_sha256=checker.SPEC_SHA, settings_sha256=hashlib.sha256(public).hexdigest(), + stage_before=expected, stage_after=r.inventory(stage,stage=True), + default_before=protected, default_after=r.default_player(root), + receipt_sha256=r.sha(phase_out/(label+'-receipt.json')))) + phases.append('runs/'+label+'/'+label+'-receipt.json') + r.require(not row['failed'], 'strict phase failed: '+label) + try: + phase('import', None); phase('parse', None) + for layout in ('A', 'B'): + directory = out/('layout-'+layout) + os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory)) + phase('layout-'+layout.lower(), 'tests/ui/test_p23_life_observation.gd') + cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)}) + for layout in ('A', 'B'): + checker.inspect_stream(out/('layout-'+layout), root) + if args.input_mode == 'native_os': + os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer) + checker = module('p23_os_input_map', root/'tools/check_p23_life_observation_evidence.py') + os_proof = 'os-proof.json' + r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root), + 'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'}) + except BaseException as caught: + error = type(caught).__name__+': '+str(caught) + finally: + cleanup, cleanup_errors = outer.finish_captured(outer.OWNED) + for key,value in saved_env.items(): + if value is None: os.environ.pop(key, None) + else: os.environ[key] = value + if r.source_refs(root) != refs or r.default_player(root) != protected: + error = (error+'; ' if error else '')+'protected root/profile changed' + if checker.input_map(root) != inputs_before: + error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed' + r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors, + 'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'}) + checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py') + r.write(out/'index.json', {'schema':'p23-life-observation-v2', 'commit':commit, + 'composition':composition, + 'source_map':selected['map'], 'input_map':inputs_before, + 'provenance':checker.input_provenance(root), + 'marker_contract_sha256':r.sha(root/CONTRACT), + 'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P23-03.json').read_text().strip().encode()).hexdigest(), + 'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS), + 'artifacts':{str(p.relative_to(out)):r.sha(p) + for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts}, + 'os_proof':os_proof, 'regression':None, + 'acceptance':'pending', 'collection_error':error}) + return 1 if error or cleanup_errors else 0 + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--project-root', type=absolute, required=True) + parser.add_argument('--output', type=absolute, required=True) + parser.add_argument('--commit', required=True) + parser.add_argument('--root-composition', type=absolute, required=True, + help='Root external S/M/T/Q and complete tested-tree blob bindings') + parser.add_argument('--godot', default='godot') + parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler') + parser.add_argument('--collect', action='store_true', required=True) + return collect(parser.parse_args(argv)) + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py new file mode 100644 index 0000000..53b292d --- /dev/null +++ b/tools/test_p23_life_observation_evidence.py @@ -0,0 +1,2394 @@ +#!/usr/bin/env python3 +"""Local semantic-checker tests; synthetic examples are never game evidence.""" +import copy +import ast +import unittest +import sys +import contextlib +import io +import json +import hashlib +import gzip +import struct +import zlib +import tempfile +import os +import subprocess +from pathlib import Path +from unittest import mock + +sys.dont_write_bytecode = True +import check_p23_life_observation_evidence as check + + +def synthetic_repair28_fields(report, snapshot=None): + """Counterfactual unit-test projection only; never rewrite raw history.""" + report['overlay_passes'] = ['traffic', 'cabins', 'actors'] + report['shaft_footprint'] = [list(c) for c in dict.fromkeys( + (stop[0]+dx, stop[1], stop[2]) for shaft in check.elevator_report(snapshot)['shafts'] + for stop in shaft['stops'] for dx in range(max(shaft.get('width', 2), 1)))] if snapshot else [] + for cabin in report.get('cabins', []): + if snapshot is not None: + shaft = next(s for s in check.elevator_report(snapshot)['shafts'] if s['shaft_id'] == cabin['shaft_id']) + cabin.update(phase=shaft['phase'], passenger_count=len(shaft['boarded'])) + count, capacity = len(shaft['boarded']), shaft['capacity'] + cabin['badge'] = ('運転 ' if shaft['operational'] else '停止 ') + ('満員 ' if capacity > 0 and count >= capacity else '') + f'{count}/{capacity}人' + (' 開 ' if cabin['doors_open'] else ' 閉 ') + shaft['phase'] + cabin['badge_rect'] = check.badge_rectangle(cabin['badge_at'], cabin['badge']) + report['traffic_obstacles'] = check.traffic_obstacles(report) + for row in report.get('traffic', []): + row['segment_rects'] = check.traffic_segments(row['band_rect'], row['rect'], report['traffic_obstacles']) + return report + + +class Repair28GeometryTests(unittest.TestCase): + def test_one_screenshot_allowed_but_real_input_pair_kept(self): + check.require_screenshot_coverage('screenshot', [{}]) + check.require_screenshot_coverage('click', [{}, {}]) + for action, shots in [('screenshot', []), ('click', [{}]), ('wheel', [{}]), ('key', None)]: + with self.subTest(action=action), self.assertRaises(ValueError): + check.require_screenshot_coverage(action, shots) + + def test_overview_occlusion_keeps_density_row_and_safe_order(self): + report = LifeOverlayTests().report() + report['actor_drawings'] = [] + report['badges'] = [dict(at=[0,y], text='cover') for y in (0,14,28,42,56,70,84)] + synthetic_repair28_fields(report) + self.assertEqual(report['traffic'][0]['segment_rects'], []) + self.assertEqual(report['traffic'][0]['traffic']['current_density'], 1) + check.validate_life_overlay(report) + source = (check.ROOT/'scenes/main/world_preview.gd').read_text() + broken = source.replace('canvas.draw_rect(rect, Color("34413b"))', + 'canvas.draw_rect(rect, Color("34413b", 0.5))') + with mock.patch.object(Path, 'read_text', return_value=broken): + with self.assertRaisesRegex(ValueError, 'opaque later badges'): + check.validate_life_overlay(report) + + def test_r15_empty_cabin_rejects_queue_guest_even_if_marked_riding(self): + fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/repair28_r15_negative.json').read_text()) + cabin = dict(rect=fixture['cabin_rect'], passengers=[], passenger_count=fixture['passenger_count']) + actor = dict(fixture['actor'], riding=True, mechanical_pass=True) + with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin'): + check.validate_cabin_intrusion(dict(cabins=[cabin], actor_drawings=[actor])) + cabin['passengers'] = [actor] + with self.assertRaisesRegex(ValueError, 'passenger drawn in empty cabin'): + check.validate_cabin_intrusion(dict(cabins=[cabin], actor_drawings=[])) + + def test_r15_traffic_is_split_against_cabin_and_badge(self): + fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/repair28_r15_negative.json').read_text()) + row = fixture['traffic'] + obstacles = [fixture['cabin_rect'], check.badge_rectangle(**fixture['queue_badge'])] + for obstacle in obstacles: + self.assertTrue(check.intersects(row['band_rect'], obstacle)) + pieces = check.traffic_segments(row['band_rect'], row['rect'], obstacles) + self.assertTrue(pieces) + for piece in pieces: + for obstacle in obstacles: + self.assertFalse(check.intersects(piece, obstacle)) + + def test_exact_subtraction_and_full_cover_fallback(self): + self.assertEqual(check.traffic_segments([10,85,60,5], [0,0,80,96], + [[20,0,20,90], [45,80,20,14]]), [[10,85,10,5], [40,85,5,5], [65,85,5,5]]) + self.assertEqual(check.traffic_segments([10,85,60,5], [0,0,80,96], + [[0,80,80,16]]), [[10,0,60,5]]) + + def test_claimed_safe_segments_and_obstacles_are_recomputed(self): + report = LifeOverlayTests().report() + report['landing_badges'] = [dict(at=[20,80], text='queue')] + synthetic_repair28_fields(report) + check.validate_life_overlay(report) + for field in ('segment_rects', 'traffic_obstacles'): + bad = copy.deepcopy(report) + if field == 'segment_rects': + bad['traffic'][0][field] = [bad['traffic'][0]['band_rect']] + else: + bad[field] = [] + with self.subTest(field=field), self.assertRaisesRegex(ValueError, 'traffic (segment|obstacle) geometry'): + check.validate_life_overlay(bad) + + def test_width_two_walkable_interior_is_hidden_not_a_passenger(self): + fixture = SpatialTests() + fixture.setUp() + fixture.test_missing_queue_overflow_rejected() + # The old implementation assigned x+1 to g0. Preserve the typed demand + # and assert that fabricating that old visible actor is now rejected. + fixture.report['landing_badges'][0]['overflow'] = 3 + fixture.report['queue_positions'] = {'guest/g0': [100,82]} + with self.assertRaisesRegex(ValueError, 'queue_positions'): + check.compare_queues(fixture.snapshot, fixture.report) + fixture.report['queue_positions'] = {} + fixture.report['shaft_footprint'].pop(1) + with self.assertRaisesRegex(ValueError, 'shaft footprint'): + check.compare_queues(fixture.snapshot, fixture.report) + + def test_cabin_state_false_fields_rejected_on_both_layers(self): + fixture = SpatialTests() + fixture.setUp() + fixture.test_cabin_geometry_and_phase() + for layer in (0,1): + fixture.report['layer'] = layer + fixture.runtime['elevators']['shafts'][0]['config']['stops'][0][2] = layer + for field, value in [('passenger_count',1), ('operational',False), ('phase','PICKUP'), + ('doors_open',False), ('badge_rect',[80,58,1,14])]: + bad = copy.deepcopy(fixture.report) + bad['cabins'][0][field] = value + with self.subTest(layer=layer,field=field), self.assertRaises(ValueError): + check.compare_cabin_geometry(fixture.snapshot, bad) + + +class Repair28CompositionTests(unittest.TestCase): + """No Git subprocess, repository writes, engine or proof generation.""" + def setUp(self): + self.s, self.m, self.t, self.q = ('1'*40, '2'*40, '3'*40, '4'*40) + base_marker = dict(schema_version=1, rows=[dict(test='old', required=True)]) + s_marker = copy.deepcopy(base_marker) + s_marker['rows'].append(dict(test='tests/ui/test_p23_life_observation.gd', required=True)) + m_marker = copy.deepcopy(base_marker) + m_marker['rows'].append(dict(test='P18', required=True)) + self.documents = [base_marker, s_marker, m_marker, check.marker_join(base_marker,s_marker,m_marker)] + names = ['project.godot','export_presets.cfg','Makefile','AGENTS.md', check.PLAN, check.CONTRACT, + 'tests/business/test_required_kitchen.gd','tools/regression/suite-policy.json', + 'scenes/main/world_preview.gd'] + base = {name: (name+' baseline').encode() for name in names} + self.files = {r: copy.deepcopy(base) for r in (check.BASE_COMMIT,self.s,self.m,self.t)} + for revision, document in zip(self.files, self.documents): + self.files[revision][check.CONTRACT] = json.dumps(document).encode() + self.files[self.s]['scenes/main/world_preview.gd'] = b'S drawing' + self.files[self.m]['tools/regression/suite-policy.json'] = b'M only policy' + self.files[self.m]['tests/business/test_required_kitchen.gd'] = b'M approved QA source' + self.files[self.t].update({p:self.files[self.s][p] for p in ['scenes/main/world_preview.gd']}) + self.files[self.t].update({p:self.files[self.m][p] for p in + ['tools/regression/suite-policy.json','tests/business/test_required_kitchen.gd']}) + self.source = {p:hashlib.sha256(v).hexdigest() for p,v in self.files[self.t].items() + if p not in (check.PLAN, check.CONTRACT)} + self.inputs = {check.CONTRACT:hashlib.sha256(self.files[self.t][check.CONTRACT]).hexdigest()} + self.roles = dict(submitted_sha=self.s, main_context_sha=self.m, tested_tree=self.t, tested_commit=self.q, + blob_bindings={p:self.blob(v) for p,v in self.files[self.t].items()}) + self.merge_tree = self.t + self.calls = [] + + @staticmethod + def blob(value): + return hashlib.sha1(b'blob '+str(len(value)).encode()+b'\0'+value).hexdigest() + + def git(self, *args): + self.calls.append(args) + if args[0] == 'cat-file': return b'tree\n' if args[2] == self.t else b'commit\n' + if args[0] == 'rev-parse': return (self.q if args[1] == 'HEAD' else self.t).encode()+b'\n' + if args[0] == 'status': return b'' + if args[0] == 'merge-tree': return self.merge_tree.encode()+b'\n' + if args[0] == 'ls-tree': + return b''.join(('100644 blob '+self.blob(value)+'\t'+path+'\0').encode() + for path,value in self.files[args[-1]].items()) + if args[0] == 'show': + revision,path = args[1].split(':',1) + return self.files[revision][path] + raise AssertionError(args) + + def batch(self, oids): + self.calls.append(('cat-file', '--batch')) + blobs = {self.blob(v): v for files in self.files.values() for v in files.values()} + return io.BytesIO(b''.join(oid.encode()+b' blob '+str(len(blobs[oid])).encode()+b'\n' + +blobs[oid]+b'\n' for oid in oids)), 0 + + def verify(self): + original = Path.read_bytes + def read(path): + if path.is_relative_to(check.ROOT): + return self.files[self.t][str(path.relative_to(check.ROOT))] + return original(path) + with mock.patch.object(Path, 'read_bytes', read): + return check.verify_composition(check.ROOT,self.roles,self.source,self.inputs,self.git, + read_batch=self.batch) + + def test_valid_route_b_keeps_unchanged_allowed_path_from_main(self): + self.assertEqual(self.verify()['tested_tree'], self.t) + self.assertIn(('merge-tree','--write-tree',self.s,self.m),self.calls) + + def test_out_of_scope_submitted_change_rejected(self): + self.files[self.s]['autoload/simulation_runtime.gd'] = b'unauthorized' + with self.assertRaisesRegex(ValueError,'outside allowed_paths'): self.verify() + + def test_wrong_merge_tree_rejected(self): + self.merge_tree = '5'*40 + with self.assertRaisesRegex(ValueError,'deterministic merge-tree'): self.verify() + + def test_conflicted_merge_fails_closed(self): + original = self.git + def conflict(*args): + if args[0] == 'merge-tree': raise ValueError('Git composition failed/conflicted') + return original(*args) + with self.assertRaisesRegex(ValueError,'conflicted'): + check.verify_composition(check.ROOT,self.roles,self.source,self.inputs,conflict) + + def test_s_only_and_main_only_blob_provenance(self): + for path in ('scenes/main/world_preview.gd','tools/regression/suite-policy.json', + 'tests/business/test_required_kitchen.gd'): + with self.subTest(path=path): + original = self.files[self.t][path] + self.files[self.t][path] = self.files[check.BASE_COMMIT][path] + with self.assertRaisesRegex(ValueError,'blob/mode provenance'): self.verify() + self.files[self.t][path] = original + + def test_structural_join_preserves_all_main_rows_and_metadata(self): + base,s,m,t = self.documents + for mutation in ('delete_P18','overwrite_P18','extra','type','metadata','duplicate_P23'): + bad = copy.deepcopy(t) + if mutation == 'delete_P18': bad['rows'].pop(1) + if mutation == 'overwrite_P18': bad['rows'][1]['required'] = False + if mutation == 'extra': bad['rows'].append(dict(test='unapproved')) + if mutation == 'type': bad['schema_version'] = True + if mutation == 'metadata': bad['extra'] = 'changed' + if mutation == 'duplicate_P23': bad['rows'].append(copy.deepcopy(bad['rows'][-1])) + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.verify_marker_join(base,s,m,bad) + positioned = copy.deepcopy(t) + positioned['rows'].insert(1, positioned['rows'].pop()) + check.verify_marker_join(base,s,m,positioned) + with self.assertRaisesRegex(ValueError,'duplicate JSON key'): + check.strict_json('{"rows":[],"rows":[]}') + + def test_binding_completeness_and_old_source_relabel_rejected(self): + for mutation in ('missing_blob','wrong_blob','extra_blob','old_source','omitted_QA'): + original_roles, original_source = copy.deepcopy(self.roles),dict(self.source) + path = 'tests/business/test_required_kitchen.gd' + if mutation == 'missing_blob': self.roles['blob_bindings'].pop(path) + if mutation == 'wrong_blob': self.roles['blob_bindings'][path] = '0'*40 + if mutation == 'extra_blob': self.roles['blob_bindings']['unbound'] = '0'*40 + if mutation == 'old_source': self.source[path] = hashlib.sha256(self.files[self.s][path]).hexdigest() + if mutation == 'omitted_QA': self.source.pop(path) + with self.subTest(mutation=mutation), self.assertRaises(ValueError): self.verify() + self.roles, self.source = original_roles, original_source + + +class Repair31StreamTests(unittest.TestCase): + def test_owned_child_cleanup_on_parse_failure_and_early_close(self): + oid = check.blob_oid(b'x') + for malformed in (True, False): + child = mock.Mock() + child.stdin = io.BytesIO() + child.stdout = io.BytesIO(b'bad\n' if malformed else oid.encode()+b' blob 1\nx\n') + child.poll.return_value = None + child.wait.return_value = -9 + with mock.patch.object(subprocess, 'Popen', return_value=child): + stream = check.git_blob_stream(check.ROOT, [oid]) + if malformed: + with self.assertRaises(ValueError): next(stream) + else: + self.assertEqual(next(stream), (oid, b'x')) + stream.close() + child.kill.assert_called_once_with() + child.wait.assert_called_once_with() + self.assertTrue(child.stdin.closed) + self.assertTrue(child.stdout.closed) + + def test_strict_stream_negatives_and_repeated_oids(self): + content = b'abc' + oid = check.blob_oid(content) + other = check.blob_oid(b'def') + record = oid.encode()+b' blob 3\nabc\n' + def read(data, oids=(oid,), status=0): + return list(check.git_blob_stream(check.ROOT, oids, + lambda requested: (io.BytesIO(data), status))) + self.assertEqual(read(record*2, (oid, oid)), [(oid, content)]*2) + invalid = [record[:-2], record[:-1], record.replace(b'blob 3', b'blob 2'), + record.replace(b'blob 3', b'blob 4'), record.replace(b'blob', b'tree'), + oid.encode()+b' missing\n', oid.encode()+b' ambiguous\n', + record+record, record.replace(b'abc', b'def'), b'', + record.replace(oid.encode(), other.encode())] + for data in invalid: + with self.subTest(data=data), self.assertRaises(ValueError): + read(data) + with self.assertRaises(ValueError): + read(record, status=1) + with self.assertRaises(ValueError): + read(record*2, (oid, other)) + + def test_checkout_paths_and_modes(self): + oid = check.blob_oid(b'x') + roles = dict(tested_tree='1'*40) + for path, mode, kind, duplicate in [('a', '100644', 'blob', True), + ('a','120000','blob',False), ('a','160000','commit',False), + ('/a','100644','blob',False), ('a/../b','100644','blob',False)]: + record = f'{mode} {kind} {oid}\t{path}\0'.encode() + with self.subTest(path=path, mode=mode), self.assertRaises(ValueError): + with check.tested_checkout(check.ROOT, roles, + lambda *args: record*(2 if duplicate else 1), + lambda oids: (io.BytesIO(oid.encode()+b' blob 1\nx\n'),0)): + self.fail('unsafe checkout yielded') + + def test_view_authority_and_binding_recheck(self): + fixture = Repair28CompositionTests() + fixture.setUp() + def verify(view): + return check.verify_composition(check.ROOT, fixture.roles, fixture.source, + fixture.inputs, fixture.git, view=view, read_batch=fixture.batch) + with tempfile.TemporaryDirectory() as temporary: + with self.assertRaisesRegex(ValueError, 'unregistered'): + verify(Path(temporary)) + with check.tested_checkout(check.ROOT, fixture.roles, fixture.git, fixture.batch) as view: + registered = check._VERIFIED_VIEWS[view.resolve()] + check._VERIFIED_VIEWS[view.resolve()] = ('0'*40, registered[1]) + with self.assertRaisesRegex(ValueError, 'tree mismatch'): verify(view) + check._VERIFIED_VIEWS[view.resolve()] = (registered[0], {}) + with self.assertRaisesRegex(ValueError, 'manifest mismatch'): verify(view) + check._VERIFIED_VIEWS[view.resolve()] = registered + # A normative document is outside source_refs; its bytes must still be checked. + (view/check.PLAN).write_bytes(b'tampered') + with mock.patch.object(Path, 'read_bytes', autospec=True) as reader: + # Preserve real view reads and synthesize only the source checkout comparison. + original = io.open + def read(path): + if path.is_relative_to(view): + with original(path, 'rb') as stream: return stream.read() + return fixture.files[fixture.t][str(path.relative_to(check.ROOT))] + reader.side_effect = read + with self.assertRaisesRegex(ValueError, 'SHA-1 mismatch'): verify(view) + self.assertNotIn(view.resolve(), check._VERIFIED_VIEWS) + + def test_constant_calls_full_composition_with_5000_paths(self): + fixture = Repair28CompositionTests() + fixture.setUp() + additions = {f'assets/bulk/{i:05}.txt': b'repeated small blob' for i in range(5000)} + for files in fixture.files.values(): files.update(additions) + fixture.source.update({p: hashlib.sha256(v).hexdigest() for p,v in additions.items()}) + fixture.roles['blob_bindings'].update({p: check.blob_oid(v) for p,v in additions.items()}) + original = Path.read_bytes + verified = set() + def read(path): + if path.is_relative_to(check.ROOT): + return fixture.files[fixture.t][str(path.relative_to(check.ROOT))] + data = original(path) + if 'bulk' in path.parts: + self.assertEqual(data, b'repeated small blob') + verified.add(path.name) + return data + with mock.patch.object(Path, 'read_bytes', read): + with check.tested_checkout(check.ROOT, fixture.roles, fixture.git, fixture.batch) as view: + check.verify_composition(check.ROOT, fixture.roles, fixture.source, fixture.inputs, + fixture.git, view=view, read_batch=fixture.batch) + self.assertEqual(len(verified), 5000) + self.assertEqual(fixture.calls.count(('cat-file','--batch')), 1) + self.assertEqual(len([c for c in fixture.calls if c[0] == 'show']), 4) + + def test_real_pipe_overflow_requests_and_repeated_paths(self): + runtime = os.environ.get('TMPDIR') + self.assertTrue(runtime, 'assigned TMPDIR required') + with tempfile.TemporaryDirectory(dir=runtime, prefix='p23-repair31-') as temporary: + repo = Path(temporary) + def git(*args, data=None): + return subprocess.run(['git','-C',str(repo),*args], input=data, + capture_output=True, check=True, timeout=30).stdout + git('init','-q') + content = b'pipe pacing verified\n' + oid = git('hash-object','-w','--stdin', data=content).decode().strip() + count = 5000 + tree = git('mktree', data=b''.join( + f'100644 blob {oid}\tpath-{i:05}\n'.encode() for i in range(count))).decode().strip() + # A watchdog bounds a deadlock regression; elapsed time is not a pass metric. + program = '''import sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +import check_p23_life_observation_evidence as c +repo, tree = Path(sys.argv[2]), sys.argv[3] +with c.tested_checkout(repo, {'tested_tree': tree}) as view: + paths = list(view.iterdir()) + assert len(paths) == 5000 + assert all(p.read_bytes() == b'pipe pacing verified\\n' for p in paths) +''' + subprocess.run([sys.executable, '-B', '-c', program, str(check.ROOT/'tools'), + str(repo), tree], check=True, timeout=60) + + +class Repair30EntryTests(unittest.TestCase): + """Synthetic labelled Git history; zero native or OS credit.""" + def setUp(self): + runtime = os.environ.get('TMPDIR') + self.assertTrue(runtime, 'assigned TMPDIR required') + self.temporary = tempfile.TemporaryDirectory(dir=runtime, prefix='p23-repair30-') + self.addCleanup(self.temporary.cleanup) + self.repo = Path(self.temporary.name)/'repo' + self.repo.mkdir() + self.build = Path(self.temporary.name)/'build' + self.evidence = self.build/'p23-life-observation-evidence' + self.evidence.mkdir(parents=True) + self.git('init', '-q') + self.git('config', 'user.name', 'Synthetic fixture') + self.git('config', 'user.email', 'fixture@example.invalid') + for path in ('tools/run_all_tests.py', 'tests/fixtures/p23-03/observation.gd', + check.PLAN, check.SPEC, 'docs/traceability/original/BASELINE.txt'): + self.write(path, (check.ROOT/path).read_bytes()) + for path in ('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md', + 'scenes/main/world_preview.gd', 'tests/business/test_required_kitchen.gd', + 'tools/regression/suite-policy.json'): + self.write(path, 'synthetic baseline\n') + for i in range(220): + self.write(f'assets/repair31/{i:04}.txt', f'verified path {i}\n') + self.marker = dict(schema_version=1, rows=[dict(test='history-'+str(i), required=True) for i in range(6)]) + self.write_marker(self.marker) + self.base = self.commit('synthetic base') + submitted = copy.deepcopy(self.marker) + submitted['rows'].insert(0, dict(test='tests/ui/test_p23_life_observation.gd', required=True)) + self.write_marker(submitted) + self.write('scenes/main/world_preview.gd', 'synthetic S\n') + self.s = self.commit('synthetic S') + self.git('checkout', '-q', '--detach', self.base) + main = copy.deepcopy(self.marker) + main['rows'].append(dict(test='P18-and-kitchen-QA', required=True)) + self.write_marker(main) + self.write('tests/business/test_required_kitchen.gd', 'synthetic M QA\n') + self.m = self.commit('synthetic M') + self.t = self.git('merge-tree', '--write-tree', self.s, self.m).splitlines()[0] + self.q = self.git('commit-tree', self.t, '-p', self.s, '-m', 'synthetic Q, no evidence') + self.c = self.git('commit-tree', self.t, '-p', self.m, '-p', self.s, '-m', 'synthetic C') + self.git('checkout', '-q', '--detach', self.q) + runner = check.module('p23_synthetic_source_filter', check.ROOT/'tools/run_all_tests.py') + sources = {p: row['sha256'] for p, row in runner.source_refs(self.repo).items()} + inputs = check.input_map(self.repo) + required = set(sources) | {p.split('#')[0] for p in inputs} | {check.PLAN, check.CONTRACT, + 'project.godot', 'tests/business/test_required_kitchen.gd', 'tools/regression/suite-policy.json'} + self.roles = dict(submitted_sha=self.s, main_context_sha=self.m, tested_tree=self.t, + tested_commit=self.q, blob_bindings={p:self.git('rev-parse', self.t+':'+p) for p in required}) + self.index = dict(schema='p23-life-observation-v2', source_map=sources, input_map=inputs, + composition={k:self.roles[k] for k in ('submitted_sha','main_context_sha','tested_tree','tested_commit')}, + commit=self.q, spec_sha256=check.SPEC_SHA, + marker_contract_sha256=check.sha(self.repo/check.CONTRACT), artifacts={}, phases=[]) + self.publish() + self.git('checkout', '-q', '--detach', self.s) + self.patch = mock.patch.object(check, 'BASE_COMMIT', self.base) + self.patch.start() + self.addCleanup(self.patch.stop) + + def git(self, *args): + result = subprocess.run(['git', '-C', str(self.repo), *args], capture_output=True, check=True) + return result.stdout.decode().strip() + + def write(self, path, content): + target = self.repo/path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(content.encode() if isinstance(content, str) else content) + + def write_marker(self, value): + self.write(check.CONTRACT, json.dumps(value, indent=2)+'\n') + + def commit(self, message): + self.git('add', '.') + self.git('commit', '-q', '-m', message) + return self.git('rev-parse', 'HEAD') + + def publish(self): + context = self.evidence/'root-composition.json' + context.write_text(json.dumps(self.roles)) + self.index['root_composition_sha256'] = check.sha(context) + (self.evidence/'index.json').write_text(json.dumps(self.index)) + + def cli(self, verify=True, extra_env=None, extra_args=()): + env = dict(BUILD_OUTPUT_DIR=str(self.build)) + env.update(extra_env or {}) + with mock.patch.dict(os.environ, env, clear=True), contextlib.redirect_stdout(io.StringIO()) as output: + code = check.main((['--verify-only'] if verify else []) + ['--project-root', str(self.repo)] + list(extra_args)) + self.assertEqual(code, 1) + result = json.loads(output.getvalue()) + self.assertEqual(result['acceptance'], 'pending') + return result['deficits'] + + def assert_entry(self, verify=True): + deficits = self.cli(verify) + self.assertTrue(any('native evidence deficit' in d for d in deficits), deficits) + self.assertTrue(any('missing OS A-writer process receipt' in d for d in deficits), deficits) + for condition in check.CONDITIONS: + self.assertTrue(any(d.startswith(condition+':') for d in deficits), deficits) + self.assertFalse(any('invalid evidence index' in d for d in deficits), deficits) + + def test_normal_and_verify_only_S_Q_C_keep_all_seven_blockers(self): + for head in (self.s, self.q, self.c): + self.git('checkout', '-q', '--detach', head) + for verify in (False, True): + with self.subTest(head=head, verify=verify): + self.assert_entry(verify) + + def test_whole_entry_has_constant_git_calls_and_verifies_every_path(self): + spawn = subprocess.Popen + calls, read_paths = [], set() + original = Path.read_bytes + def counted(*args, **kwargs): + calls.append(args[0]) + return spawn(*args, **kwargs) + def read(path): + data = original(path) + if 'repair31' in path.parts: + self.assertEqual(data, f'verified path {int(path.stem)}\n'.encode()) + read_paths.add(path.name) + return data + with mock.patch.object(subprocess, 'Popen', counted), mock.patch.object(Path, 'read_bytes', read): + self.assert_entry() + self.assertEqual(len(read_paths), 220) + self.assertEqual(sum(command[-2:] == ['cat-file','--batch'] for command in calls), 1) + self.assertEqual(sum('show' in command for command in calls), 4) + + def test_wrong_roles_and_missing_Q(self): + for key, value in (('submitted_sha',self.base), ('main_context_sha',self.base), + ('tested_tree',self.git('rev-parse', self.base+'^{tree}')), + ('tested_commit','0'*40)): + original = self.roles[key] + self.roles[key] = value + self.publish() + with self.subTest(key=key): + self.assertTrue(any('invalid evidence index' in d for d in self.cli())) + self.roles[key] = original + + def test_dirty_roles_and_source_QA_rollback(self): + for head in (self.s,self.q,self.c): + self.git('checkout', '-q', '--detach', head) + for path in ('scenes/main/world_preview.gd','tests/business/test_required_kitchen.gd'): + original = (self.repo/path).read_bytes() + self.write(path, 'synthetic rollback\n') + with self.subTest(head=head,path=path): + self.assertTrue(any('dirty' in d for d in self.cli())) + self.write(path, original) + + def test_unexpected_C_parents_and_tree(self): + wrong_tree = self.git('rev-parse',self.base+'^{tree}') + for tree, parents in ((self.t,[self.s,self.m]), (self.t,[self.m,self.s,self.base]), + (self.t,[self.m]), (wrong_tree,[self.m,self.s])): + args = ['commit-tree', tree, '-m', 'synthetic invalid C'] + for parent in parents: + args.extend(['-p',parent]) + head = self.git(*args) + self.git('checkout','-q','--detach',head) + with self.subTest(tree=tree,parents=parents): + self.assertTrue(any('C parents' in d or 'C tree' in d for d in self.cli())) + + def test_context_missing_mismatch_symlink_and_environment_ignored(self): + context = self.evidence/'root-composition.json' + original = context.read_bytes() + context.unlink() + self.assertTrue(any('context' in d for d in self.cli(extra_env={'P23_ROOT_COMPOSITION':str(self.repo/'unused')}))) + context.write_bytes(original+b' ') + self.assertTrue(any('SHA mismatch' in d for d in self.cli())) + context.unlink() + target = Path(self.temporary.name)/'context.json' + target.write_bytes(original) + context.symlink_to(target) + self.assertTrue(any('unsafe' in d for d in self.cli())) + context.unlink() + context.write_bytes(original) + alternate = Path(self.temporary.name)/'alternate.json' + alternate.write_text('{}') + self.assertTrue(any('disagrees' in d for d in self.cli(extra_args=['--root-composition',str(alternate)]))) + self.assertTrue(any('native evidence deficit' in d for d in self.cli(extra_env={'P23_ROOT_COMPOSITION':str(alternate)}))) + self.index.pop('root_composition_sha256') + (self.evidence/'index.json').write_text(json.dumps(self.index)) + self.assertTrue(any('SHA mismatch' in d for d in self.cli())) + + +class ActorNonoverlapTests(unittest.TestCase): + def report(self, second_at): + return dict(actor_drawings=[ + dict(key='guest/a', at=[0, 0], scale=1, mode='idle', frame=0, flip=False), + dict(key='staff/b', at=second_at, scale=1, mode='idle', frame=0, flip=False)], cabins=[]) + + def test_edge_contact_is_not_overlap(self): + for at in ([6, 0], [0, 12], [-6, 0], [0, -12]): + with self.subTest(at=at): + check.validate_actor_nonoverlap(self.report(at)) + + def test_overlap_ignores_asserted_verdicts_and_offscreen_labels(self): + report = self.report([5, 11]) + for actor in report['actor_drawings']: + actor.update(mechanical_pass=True, eligibility='offscreen', rect=[999, 999, 1, 1]) + with self.assertRaisesRegex(ValueError, 'actor-actor core overlap: guest/a / staff/b'): + check.validate_actor_nonoverlap(report) + + def test_cabin_passenger_is_checked_against_floor_actor(self): + report = self.report([0, 0]) + report['cabins'] = [dict(passengers=[report['actor_drawings'].pop()])] + with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'): + check.validate_actor_nonoverlap(report) + + def test_invalid_footprints_are_not_silently_skipped(self): + for scale in (0, -1, float('nan'), float('inf'), True): + report = self.report([100, 100]) + report['actor_drawings'][0]['scale'] = scale + with self.subTest(scale=scale), self.assertRaisesRegex(ValueError, 'invalid actor footprint'): + check.validate_actor_nonoverlap(report) + + def test_r13_false_row_11_overlaps_staff_row_29(self): + fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/r13_actor_overlap.json').read_text()) + self.assertEqual(fixture['independent_judgment'], [11, False, False, False]) + with self.assertRaisesRegex(ValueError, + 'actor-actor core overlap: guest/guest-auto-00000041 / staff/staff_gina'): + check.validate_actor_nonoverlap(fixture['report_inputs']) + # Exercise the actual offline evidence gate, before any audit assertions. + with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'): + check.validate_drawing_rows(fixture['report_inputs'], {'rows': []}) + + def test_overlapping_full_boxes_with_disjoint_cores_pass(self): + # Both 16x18 boxes overlap by 9x18, but their six-pixel cores do not. + check.validate_actor_nonoverlap(self.report([7, 0])) + + def test_walk_bob_is_part_of_core(self): + report = self.report([0, 12]) + report['actor_drawings'][1].update(mode='walk', frame=2) + with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'): + check.validate_actor_nonoverlap(report) + report['actor_drawings'][1]['frame'] = 3 + check.validate_actor_nonoverlap(report) + + def test_flipped_core_and_lying_breathing_bounds(self): + actor = self.report([0, 0])['actor_drawings'][0] + actor.update(flip=True, scale=3) + self.assertEqual(check.actor_identity_core(actor), [12, 3, 18, 36]) + actor.update(mode='standard_room_sleep', frame=2) + self.assertEqual(check.actor_identity_core(actor), [3, 30, 39, 18]) + actor['frame'] = 5 + self.assertEqual(check.actor_identity_core(actor), [3, 33, 39, 15]) + actor.update(mode='collapsed', flip=False) + self.assertEqual(check.actor_identity_core(actor), [6, 33, 39, 15]) + + def test_missing_pose_is_rejected(self): + report = self.report([100, 0]) + del report['actor_drawings'][0]['frame'] + with self.assertRaisesRegex(ValueError, 'missing actor pose'): + check.validate_actor_nonoverlap(report) + + def test_duplicate_actor_is_rejected(self): + report = self.report([100, 0]) + report['actor_drawings'][1]['key'] = 'guest/a' + with self.assertRaisesRegex(ValueError, 'duplicate actor drawing'): + check.validate_actor_nonoverlap(report) + + def test_forged_transit_flag_does_not_exempt_overlap(self): + report = self.report([0, 0]) + report['actor_drawings'][1]['in_transit'] = True + with self.assertRaisesRegex(ValueError, 'actor-actor core overlap'): + check.validate_actor_nonoverlap(report) + + def test_core_table_requires_exact_art_source(self): + with mock.patch.object(check, 'PIXEL_PEOPLE_CORE_SHA256', '0' * 64): + with self.assertRaisesRegex(ValueError, 'PixelPeople core source SHA256'): + check.validate_actor_nonoverlap(self.report([100, 0])) + + def test_recorded_service_region_and_logical_cell_have_no_common_area(self): + # Diagnostic of the unresolved simultaneous-bounds requirement, not + # permission to hide these occupants or bypass containment checks. + fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/r13_service_region_gap.json').read_text()) + for guest in fixture['guests']: + traffic = next(r for r in fixture['traffic'] if r['cell'] == guest['cell']) + x, y, w, h = traffic['rect'] + cell = [x - 2, y - 2, w + 4, h + 4] # get_cell_rect(...).grow(-2) + pixel = cell[2] / 16 + x, y, w, h = fixture['room_rects'][guest['target']] + region = [x + pixel, y + pixel, w - 2 * pixel, h - 2 * pixel] + with self.subTest(guest=guest['id']): + self.assertLessEqual(min(cell[0] + cell[2], region[0] + region[2]) + - max(cell[0], region[0]), 0) + + +class LifeOverlayTests(unittest.TestCase): + def report(self): + return synthetic_repair28_fields(dict(observation_serial=7, overlay_consumed_serial=7, + overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2], + actor_drawings=[dict(key='guest/a', at=[20, 30], scale=3, + mode='idle', frame=0, flip=False)], cabins=[], + traffic=[dict(rect=[2, 2, 60, 92], band_rect=[9.2, 85.5, 45.6, 5], + traffic=dict(current_density=1, temporary_block=False))])) + + def test_front_and_back_valid_consumed_band(self): + for layer in (0, 1): + report = self.report() + report['layer'] = layer + check.validate_life_overlay(report) + + def test_stale_serial_fails(self): + report = self.report() + report['overlay_consumed_serial'] = 6 + with self.assertRaisesRegex(ValueError, 'stale LifeOverlay serial'): + check.validate_life_overlay(report) + + def test_band_after_actors_fails(self): + report = self.report() + report['overlay_passes'].reverse() + with self.assertRaisesRegex(ValueError, 'LifeOverlay pass order'): + check.validate_life_overlay(report) + + def test_overlay_below_inactive_art_fails(self): + report = self.report() + report['overlay_child_indices'] = [1, 0, 2] + with self.assertRaisesRegex(ValueError, 'LifeOverlay child order'): + check.validate_life_overlay(report) + + def test_core_crossed_by_actual_band_fails(self): + report = self.report() + report['actor_drawings'][0]['at'][1] = 60 + with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band'): + check.validate_life_overlay(report) + + def test_forged_band_geometry_fails(self): + report = self.report() + report['traffic'][0]['band_rect'][1] = 50 + with self.assertRaisesRegex(ValueError, 'traffic band rectangle'): + check.validate_life_overlay(report) + + def test_report_cannot_excuse_wrong_source_order(self): + source = (check.ROOT / 'scenes/main/world_preview.gd').read_text() + # Claimed pass order stays valid; actual callback source is inverted. + broken = source.replace('_draw_congestion_overlay(_life_overlay)', 'SWAP') + broken = broken.replace('_draw_actors(_life_overlay)', '_draw_congestion_overlay(_life_overlay)') + broken = broken.replace('SWAP', '_draw_actors(_life_overlay)') + with mock.patch.object(Path, 'read_text', return_value=broken): + with self.assertRaisesRegex(ValueError, 'LifeOverlay source pass order'): + check.validate_life_overlay(self.report()) + + +class ProjectionBandTests(unittest.TestCase): + def setUp(self): + fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/repair28_frame180_projection.json').read_text()) + self.report, self.snapshot = fixture['report'], fixture['snapshot'] + synthetic_repair28_fields(self.report, self.snapshot) + + def occupied(self): + for row in self.report['traffic']: + if row['cell'] in ([0, 1, 0], [1, 1, 0]): + self.assertEqual(row['traffic']['current_density'], 0) + row['traffic']['current_density'] = 1 + + def test_counterfactual_actual_frame180_density_one(self): + """COUNTERFACTUAL density substitution; never occupied-band engine evidence.""" + self.occupied() + for key in ('guest/p23-018', 'guest/p23-019'): + self.assertTrue(any(a['key'] == key for c in self.report['cabins'] for a in c['passengers'])) + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_cabin_passenger_union_fully_occludes_band(self): + self.occupied() + row = next(r for r in self.report['traffic'] if r['cell'] == [0, 1, 0]) + x, y, w, h = row['band_rect'] + # Each core covers only part; their touching intervals cover the union. + for i, actor in enumerate(self.report['cabins'][0]['passengers']): + actor.update(scale=2, flip=False, mode='idle', at=[x+i*12-12, y-2]) + with self.assertRaisesRegex(ValueError, 'traffic band fully occluded:'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_spoofed_cabin_rectangle_fails_raw_recomputation(self): + self.occupied() + self.report['cabins'][0]['rect'][1] += 1 + with self.assertRaisesRegex(ValueError, 'cabin/rect'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_flag_only_cabin_projection_without_raw_membership_fails(self): + self.occupied() + for cabin in self.report['cabins']: + for actor in cabin['passengers']: + actor['key'] = 'guest/spoof-' + actor['key'] + actor['riding'] = True + with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_no_raw_state_never_exempts_cabin_projection(self): + self.occupied() + with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band'): + check.validate_life_overlay(self.report) + + def test_floor_copy_of_raw_cabin_member_keeps_zero_intersection(self): + self.occupied() + self.report['actor_drawings'].append(copy.deepcopy(self.report['cabins'][0]['passengers'][0])) + with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_blocked_band_uses_same_projection_and_legibility_rules(self): + row = next(r for r in self.report['traffic'] if r['cell'] == [0, 1, 0]) + row['traffic']['temporary_block'] = True + check.validate_life_overlay(self.report, snapshot=self.snapshot) + x, y, w, h = row['band_rect'] + for i, actor in enumerate(self.report['cabins'][0]['passengers']): + actor.update(scale=2, flip=False, mode='idle', at=[x+i*12-12, y-2]) + with self.assertRaisesRegex(ValueError, 'traffic band fully occluded:'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_flag_only_carried_projection_without_binding_fails(self): + self.occupied() + actor = copy.deepcopy(self.report['cabins'][0]['passengers'][0]) + actor.update(key='staff/spoof', staff=True, riding=False, region='carried') + self.report['actor_drawings'].append(actor) + self.report['carried_patients'] = {'spoof': 'carrier'} + with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin: staff/spoof'): + check.validate_life_overlay(self.report, snapshot=self.snapshot) + + def test_carried_projection_requires_original_binding_tracks_and_fit(self): + fixture = AnchoredGeometryTests() + fixture.setUp() + fixture.person('staff/carrier', [0, 0, 0], 'AVAILABLE') + fixture.person('staff/patient', [0, 0, 0], 'INCAPACITATED') + fixture.runtime['staff']['tasks'] = [dict(task_type='rescue_transport', state='MOVING', + assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='')] + fixture.report['carried_patients'] = {'patient': 'carrier'} + fixture.drawing('staff/carrier', [108, 214]) + fixture.drawing('staff/patient', [108, 166], 'collapsed') + for key, value in LifeOverlayTests().report().items(): + if key.startswith('overlay_') or key == 'observation_serial': + fixture.report[key] = value + # Synthetic band crosses the raised patient, with uncovered span. + fixture.report['traffic'] = [dict(cell=[0, 1, 0], rect=[78, 100.5, 60, 92], + band_rect=[85.2, 184, 45.6, 5], traffic=dict(current_density=1, temporary_block=False))] + synthetic_repair28_fields(fixture.report) + check.validate_life_overlay(fixture.report, snapshot=fixture.snapshot) + for mutation in ('binding', 'track', 'position'): + report, snapshot = copy.deepcopy(fixture.report), copy.deepcopy(fixture.snapshot) + if mutation == 'binding': snapshot['systems']['runtime']['staff']['tasks'] = [] + if mutation == 'track': report['actor_tracks']['staff/carrier']['cell'] = [1, 0, 0] + if mutation == 'position': report['actor_drawings'][1]['at'][1] += 1 + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.validate_life_overlay(report, snapshot=snapshot) + + +class SetupBudgetTests(unittest.TestCase): + def setUp(self): + self.source = (check.ROOT / 'tests/fixtures/p23-03/observation.gd').read_text() + + def test_all_planned_spends_enter_common_needed_and_deltas(self): + self.assertEqual(check.check_setup_budget_source(self.source), + {'construction', 'signing', 'purchase'}) + + def test_missing_spend_category_fails_before_os_replay(self): + expressions = { + 'construction': 'needed[key] += int(definition.construction_cost.get(key, 0))', + 'signing': 'needed[&"gold"] += signing_total', + 'purchase': 'needed[&"gold"] += purchase_total', + } + for category, expression in expressions.items(): + with self.subTest(category=category), self.assertRaisesRegex(ValueError, category): + check.check_setup_budget_source(self.source.replace(expression, 'pass')) + + def test_missing_delta_coverage_fails(self): + with self.assertRaisesRegex(ValueError, 'deltas'): + check.check_setup_budget_source(self.source.replace( + 'deltas[key] = needed[key] - state.get_resource(key)', 'pass')) + + +class QueuedElevatorTests(unittest.TestCase): + def setUp(self): + fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text()) + self.state = {'systems': {'runtime': {'elevators': fixture['saved']}}} + + def test_queued_trip_without_cabin_seat_uses_config_shaft(self): + saved = self.state['systems']['runtime']['elevators'] + self.assertNotIn('shaft_id', saved['shafts'][0]) + self.assertEqual(saved['shafts'][0]['boarded'], []) + report = check.elevator_report(self.state) + self.assertEqual(report['shafts'][0]['waiting_count'], 1) + self.assertEqual(report['shafts'][0]['passenger_count'], 0) + self.assertAlmostEqual(report['shafts'][0]['visual_floor'], .9) + self.assertEqual(report['trips'], saved['trips']) + + def test_tampered_shaft_attribution_fails(self): + self.state['systems']['runtime']['elevators']['trips'][0]['shaft_id'] = 'wrong.shaft' + with self.assertRaisesRegex(ValueError, 'shaft attribution'): + check.elevator_report(self.state) + + def test_wrong_waiting_count_fails_whole_report_comparison(self): + expected = check.elevator_report(self.state) + tampered = copy.deepcopy(expected) + tampered['shafts'][0]['waiting_count'] = 0 + directory = mock.MagicMock() + manifest = dict(label='queued-fixture', pid=123, state='state', + state_after='state', report='report') + directory.__truediv__.return_value.open.return_value = io.StringIO(json.dumps(manifest)+'\n') + # Exercise the actual stream comparison; isolate unrelated actor fields. + with mock.patch.object(check, 'read_object', side_effect=[self.state, self.state, + {'elevators': tampered}]), \ + mock.patch.object(check, 'compare_actor_rows', return_value=[]): + with self.assertRaisesRegex(ValueError, 'typed elevator report mismatch'): + check.inspect_stream(directory) + + def test_extra_and_missing_trip_keys_fail(self): + for mutation in ('extra', 'missing'): + with self.subTest(mutation=mutation): + state = copy.deepcopy(self.state) + trip = state['systems']['runtime']['elevators']['trips'][0] + if mutation == 'extra': + trip['unexpected'] = True + else: + del trip['shaft_id'] + with self.assertRaisesRegex(ValueError, 'exact elevator trip keys'): + check.elevator_report(state) + + +class SnapshotCellTests(unittest.TestCase): + def test_layout_arrival_compares_serialized_cell_arrays(self): + source = (check.ROOT / 'tests/fixtures/p23-03/layout_comparison.gd').read_text() + serializer = (check.ROOT / 'scripts/guest/guest_runtime.gd').read_text() + self.assertIn('&"current_cell": _cell_to_array(current_cell)', serializer) + goal = check.re.search(r'const GOAL := Vector3i\(([^)]+)\)', source) + self.assertIsNotNone(goal) + coordinates = ast.literal_eval('[' + goal.group(1) + ']') + comparison = check.re.search(r'if guest\[&"current_cell"\] != (.+?) or finished\.has', source) + self.assertIsNotNone(comparison) + expression = comparison.group(1) + self.assertEqual(expression, '[GOAL.x, GOAL.y, GOAL.z]') + for axis, value in zip('xyz', coordinates): + expression = expression.replace('GOAL.' + axis, str(value)) + target = ast.literal_eval(expression) + self.assertEqual(json.loads('[2, 1, 0]'), target) + for cell in ([2, 0, 0], [3, 1, 0], [2, 1, 1]): + self.assertNotEqual(cell, target) + + +class ImageTests(unittest.TestCase): + def test_collector_conversion_is_bound_to_each_cua_screenshot(self): + runner = check.module('p23_unit_collector', check.ROOT/'tools/run_p23_life_observation.py') + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + pair = root/'os-pair/A' + request_dir = pair/'writer-request-0001' + request_dir.mkdir(parents=True) + request = request_dir/'request.json' + request.write_text(json.dumps(dict(sequence=1, target_pid=123))) + raw = request_dir/'raw.jpg' + raw.write_bytes(b'\xff\xd8unit-only\xff\xd9') + shot = dict(path=str(raw.relative_to(root)), raw_sha256=check.sha(raw), tool_call_id='unit-shot-call') + receipt = request_dir/'cua.json' + receipt.write_text(json.dumps(dict(screenshots=[shot]))) + ack = request_dir/'ack.json' + ack.write_text(json.dumps(dict(request_sha256=check.sha(request), input_receipt=str(receipt.relative_to(root))))) + (pair/'writer-requests.json').write_text(json.dumps(dict(requests=[str(ack.relative_to(root))]))) + def convert(raw_path, png_path, cua, receipt_path): + self.assertEqual(cua, shot) + self.assertEqual(Path(raw_path).read_bytes(), b'\xff\xd8unit-only\xff\xd9') + Path(png_path).write_bytes(self.png()) + Path(receipt_path).write_text('{}') + with mock.patch.object(runner, 'module', return_value=check), \ + mock.patch.object(check, 'image_kind', return_value=('jpeg', 2, 2)), \ + mock.patch.object(runner, 'convert_screenshot', side_effect=convert) as converter: + output = runner.collect_screenshots(root, pair, 'writer') + converter.assert_called_once() + rows = json.loads((root/output).read_text()) + self.assertEqual(rows[0]['tool_call_id'], shot['tool_call_id']) + self.assertEqual(rows[0]['raw_sha256'], check.sha(raw)) + self.assertEqual(rows[0]['png_sha256'], check.sha(root/rows[0]['png_path'])) + self.assertTrue(rows[0]['conversion']) + + @staticmethod + def png(mode=0, channels=3): + def chunk(kind, body): + return struct.pack('>I', len(body)) + kind + body + struct.pack('>I', zlib.crc32(kind + body)) + rows = [bytes([10, 20, 30, 40][:channels] * 2), + bytes([50, 60, 70, 80][:channels] * 2)] + filtered = bytearray() + prior = bytes(2 * channels) + for row in rows: + filtered.append(mode) + for x, value in enumerate(row): + left, up, corner = (row[x-channels] if x >= channels else 0, + prior[x], prior[x-channels] if x >= channels else 0) + p = left + up - corner + distances = [abs(p-left), abs(p-up), abs(p-corner)] + predictors = [0, left, up, (left+up)//2, + [left, up, corner][distances.index(min(distances))]] + filtered.append((value-predictors[mode]) % 256) + prior = row + header = struct.pack('>IIBBBBB', 2, 2, 8, 2 if channels == 3 else 6, 0, 0, 0) + return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header) + + chunk(b'IDAT', zlib.compress(filtered)) + chunk(b'IEND', b'')) + + def test_all_png_filters_rgb_and_rgba(self): + for mode in range(5): + for channels in (3, 4): + with self.subTest(mode=mode, channels=channels): + w, h, rows = check.png_pixels(self.png(mode, channels)) + self.assertEqual((w, h), (2, 2)) + self.assertEqual(rows[0][0], tuple([10, 20, 30, 40][:channels])) + self.assertEqual(rows[1][1], tuple([50, 60, 70, 80][:channels])) + + def test_crc_and_trailing_bytes_rejected(self): + data = bytearray(self.png()) + data[29] ^= 1 + with self.assertRaisesRegex(ValueError, 'CRC mismatch'): + check.png_pixels(bytes(data)) + with self.assertRaisesRegex(ValueError, 'invalid IEND'): + check.png_pixels(self.png() + b'extra') + + def conversion(self): + raw, png = b'\xff\xd8synthetic-unit-test\xff\xd9', self.png() + digest = lambda b: hashlib.sha256(b).hexdigest() + rec = dict(raw_path='/raw.png', raw_sha256=digest(raw), raw_kind='jpeg', + tool_call_id='unit-test-call', converter='/usr/bin/sips', converter_sha256='a'*64, + argv=['/usr/bin/sips', '-s', 'format', 'png', '/raw.png', '--out', '/decoded.png'], + exit_code=0, stdout_sha256=digest(b''), stderr_sha256=digest(b''), + png_path='/decoded.png', png_sha256=digest(png), width=2, height=2) + cua = dict(raw_sha256=digest(raw), tool_call_id='unit-test-call') + return rec, cua, {'/raw.png': raw, '/decoded.png': png} + + def test_conversion_chain_and_specific_forgery_deficits(self): + rec, cua, files = self.conversion() + helper = mock.Mock() + helper.jpeg_dimensions.return_value = (2, 2) + with mock.patch.object(Path, 'read_bytes', lambda p: files[str(p)]), \ + mock.patch.object(check, 'module', return_value=helper): + self.assertEqual(check.inspect_conversion(rec, cua, 'a'*64)[:2], (2, 2)) + for key, value, deficit in [('converter_sha256', 'b'*64, 'converter SHA mismatch'), + ('argv', [], 'converter argv mismatch'), + ('raw_sha256', 'b'*64, 'CUA raw hash/call mismatch'), + ('width', 3, 'conversion dimensions mismatch')]: + with self.subTest(key=key): + bad = dict(rec, **{key: value}) + with self.assertRaisesRegex(ValueError, deficit): + check.inspect_conversion(bad, cua, 'a'*64) + with self.assertRaisesRegex(ValueError, 'JPEG screenshot requires conversion record'): + check.screenshot_pixels('/raw.png') + + +class RestAuditTests(unittest.TestCase): + def fixture(self): + member = dict(staff_id='s', state='OFF_DUTY', health=100, current_task_id='', current_cell=[0, 0, 0]) + facilities = [dict(instance_id=fid, definition_id='staff_waiting_room', origin=[0, 0, 0], + occupied_cells=[], state='OPEN', suspension_reason='') for fid in ('a', 'b')] + runtime = dict(staff=dict(staff=[member]), facilities=dict(instances=facilities), + defense=dict(raids=[]), events=dict(actor_locks={}), elevators=dict(trips=[]), + path=dict(cell_traffic=[dict(cell=[x, 0, 0]) for x in (-1, 0, 1)]), + world=dict(bounds=dict(min_x=-1, max_x=1, min_y=0, max_y=0), occupancies=[])) + routes = [dict(cell=[x, 0, 0], path=[[0, 0, 0], [x, 0, 0]] if x else [[0, 0, 0]], + edges=[dict(**{'from': [0, 0, 0], 'to': [x, 0, 0]}, total_cost=0)] if x else [], + calculate_path_cost=0) for x in (-1, 0, 1)] + observed = [] + for f in facilities: + observed.append({k: f[k] for k in ('instance_id', 'definition_id', 'origin', 'occupied_cells')}) + observed[-1].update(operation_state='OPERATING', probes=[dict(cell=[x, 0, 0], has_cell=True) for x in (0, -1, 1)], + ordered=[[x, 0, 0] for x in (-1, 0, 1)], routes=copy.deepcopy(routes)) + selected = dict(reachable=True, resting=True, cell=[-1, 0, 0], facility_id='a', label='待機室で休憩中') + audit = dict(path_service_sha256=check.sha(check.ROOT/'scripts/world/path_service.gd'), + staff=[dict(member, defense_memberships=[], actor_lock=False, in_transit=False, + facilities=observed, comparison_only=selected)]) + report = dict(resting_staff={'s': 'a'}, actor_drawings=[], layer=0, carried_patients={}, rooms={}, + viewport=[1440, 810], zoom=1, pan=[0, 0]) + return dict(systems=dict(runtime=runtime)), report, audit + + def test_selection_ties_and_nonmutation(self): + state, report, audit = self.fixture() + before = copy.deepcopy((state, report, audit)) + self.assertEqual(check.compare_rest(state, report, audit)['s']['cell'], [-1, 0, 0]) + self.assertEqual((state, report, audit), before) + + def test_rest_forgeries(self): + mutations = { + 'selection': lambda a: a['staff'][0]['comparison_only'].update(facility_id='b'), + 'tie': lambda a: a['staff'][0]['comparison_only'].update(cell=[0, 0, 0]), + 'lock': lambda a: a['staff'][0].update(actor_lock=True), + 'transit': lambda a: a['staff'][0].update(in_transit=True), + 'label': lambda a: a['staff'][0]['comparison_only'].update(label='宿舎で休憩中'), + 'cost': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].update(total_cost=9), + 'facility_order': lambda a: a['staff'][0]['facilities'].reverse(), + 'candidate_order': lambda a: a['staff'][0]['facilities'][0]['ordered'].reverse(), + 'empty_edge': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].clear(), + } + for name, mutate in mutations.items(): + with self.subTest(name=name): + state, report, audit = self.fixture() + mutate(audit) + with self.assertRaises(ValueError): + check.compare_rest(state, report, audit) + + def test_ineligible_and_transit_recomputed(self): + state, report, audit = self.fixture() + state['systems']['runtime']['events']['actor_locks']['s'] = 'event' + audit['staff'][0].update(actor_lock=True, comparison_only={}) + report['resting_staff'] = {} + self.assertEqual(check.compare_rest(state, report, audit), {'s': {}}) + state, report, audit = self.fixture() + state['systems']['runtime']['elevators']['trips'] = [dict(actor_id='staff:s')] + audit['staff'][0]['in_transit'] = True + audit['staff'][0]['comparison_only'].update(resting=False, label='休憩先へ移動中') + report['resting_staff'] = {} + self.assertFalse(check.compare_rest(state, report, audit)['s']['resting']) + + +class InfrastructureTests(unittest.TestCase): + def test_matrix_requires_each_condition_and_each_category(self): + executed = {c: {k: set(v) for k, v in categories.items()} for c, categories in check.REQUIRED_CHECKS.items()} + self.assertTrue(all(r['status'] == 'passed' for r in check.condition_matrix(executed))) + for condition in check.CONDITIONS: + for category in check.CATEGORIES: + with self.subTest(condition=condition, category=category): + broken = copy.deepcopy(executed) + broken[condition][category].clear() + matrix = check.condition_matrix(broken) + self.assertEqual(next(r for r in matrix if r['condition'] == condition)['status'], 'pending') + self.assertIn(category, check.BLOCKERS(broken)[condition]) + + def test_distinct_pid_ack_and_formal_hash_negatives(self): + members = {name: 'a'*64 for name in ('world.bin', 'manifest.json', 'thumbnail.png', 'journal.log')} + writer = dict(actual_pid=1) + reader = dict(actual_pid=2, formal_input={k: dict(sha256=v) for k, v in members.items()}) + saved = dict(pid=1, members=members) + records = [dict(request=dict(action='load_menu', target_pid=2, sequence=1, requested_ticks_usec=10, requested_unix_ns=90), + ack=dict(target_pid=2, sequence=1, request_sha256='b'*64, timestamp_ns=110), + observed=dict(pid=2, sequence=1, request_sha256='b'*64, ack_observed_ticks_usec=20, ack_observed_unix_ns=120, status='手動セーブを読み込みました。'), + cua=dict(target_pid=2, method='native_os', tool_call_id='actual-call', timestamp_ns=100), + request_sha256='b'*64)] + check.inspect_resume(writer, reader, saved, records, members) + for change in ('pid', 'ack', 'time', 'hash', 'id'): + with self.subTest(change=change): + r, q = copy.deepcopy(reader), copy.deepcopy(records) + if change == 'pid': r['actual_pid'] = 1 + if change == 'ack': q.clear() + if change == 'time': q[0]['observed']['ack_observed_ticks_usec'] = 9 + if change == 'hash': r['formal_input']['world.bin']['sha256'] = 'c'*64 + if change == 'id': q[0]['ack']['sequence'] = 2 + with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members) + + def test_interpolation_history_tampering(self): + state = dict(systems=dict(runtime=dict(guests=dict(guests=[]), staff=dict(staff=[]), + elevators=dict(shafts=[], trips=[], current_minute=0)))) + report = dict(animation_time=0.0, paused=True, layer=0, zoom=1, pan=[0,0], viewport=[100,100], + actor_tracks={}, elevator_tracks={}, actor_drawings=[], cabins=[]) + audit = dict(frames=[dict(frame=1, delta=.01, scaled_delta=0, display=copy.deepcopy(report)), + dict(frame=2, delta=.01, scaled_delta=0, display=copy.deepcopy(report))]) + check.compare_motion(state, report, audit) + for change in ('delta', 'frame', 'decreasing_frame', 'no_frames', 'one_frame', 'display', 'report_binding'): + with self.subTest(change=change): + bad = copy.deepcopy(audit) + if change == 'delta': bad['frames'][1]['scaled_delta'] = 1 + if change == 'frame': bad['frames'][1]['frame'] = 1 + if change == 'decreasing_frame': bad['frames'][1]['frame'] = 0 + if change == 'no_frames': bad['frames'].clear() + if change == 'one_frame': bad['frames'].pop() + if change == 'report_binding': bad['frames'][1]['display']['zoom'] = 2 + if change == 'display': bad['frames'][1]['display']['animation_time'] = 1 + with self.assertRaises(ValueError): check.compare_motion(state, report, bad) + + def test_verify_only_missing_index_is_a_concrete_deficit(self): + with mock.patch.dict(check.os.environ, {'BUILD_OUTPUT_DIR': '/missing-p23-proof'}, clear=True): + with contextlib.redirect_stdout(io.StringIO()) as output: + self.assertEqual(check.main(['--verify-only']), 1) + result = json.loads(output.getvalue()) + self.assertEqual(result['integrity'], 'fail') + self.assertEqual(result['acceptance'], 'pending') + self.assertIn('index.json', result['deficits'][0]) + self.assertNotIn('blocked', result['deficits'][0]) + + def test_verify_only_requires_build_output(self): + with mock.patch.dict(check.os.environ, {}, clear=True): + with contextlib.redirect_stderr(io.StringIO()) as output: + self.assertEqual(check.main(['--verify-only']), 2) + self.assertIn('BUILD_OUTPUT_DIR is required', output.getvalue()) + + def test_normative_plan_change_invalidates_but_progress_does_not(self): + root = check.ROOT + text = (root / check.PLAN).read_text() + original = check.input_map(root) + progress = text.splitlines(keepends=True) + progress[2] = 'Progress-only test mutation\n' + with mock.patch.object(Path, 'read_text', return_value=''.join(progress)): + self.assertEqual(check.input_map(root), original) + changed = text.replace('同需要の動線改善を観察できる。', 'altered normative acceptance') + self.assertNotEqual(changed, text) + with mock.patch.object(Path, 'read_text', return_value=changed): + self.assertNotEqual(check.input_map(root), original) + self.assertNotIn(check.PLAN, original) + self.assertIn('full_plan_sha256', check.input_provenance(root)) + for path in (root / 'tests/fixtures/p23-03').rglob('*'): + if path.is_file(): + self.assertIn(path.relative_to(root).as_posix(), original) + + def test_os_process_guards_reject_engine_profile_and_source_drift(self): + source = {'source.gd': 'a' * 64} + receipt = dict(engine_ended=True, passed=True, exit_code=0, error=None, + guard_errors=[], cleanup_errors=[], default_before={}, default_after={}, + stage_before={}, stage_after={}, source_map=source, + source_after={'source.gd': {'sha256': 'a' * 64}}, + owned_cleanup=[{'engine_ended': True}], ordinary_owner_proof={'present': True}) + receipt.update(spec_sha256=check.SPEC_SHA, inputs={'spec': 'a'*64}, inputs_after={'spec': 'a'*64}, + environment={'P23_LAYOUT': 'A'}, environment_after={'P23_LAYOUT': 'A'}, actual_pid=123, + profile='/private/test', profile_name='test', profile_before={'existed': False, 'files': {}}, profile_after={}) + for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'): + receipt[field] = receipt[field+'_after'] = 'a'*64 + check.inspect_os_receipt(receipt, source) + for field, value in [('engine_ended', False), ('default_after', {'changed': True}), + ('source_after', {'source.gd': {'sha256': 'b' * 64}}), + ('stage_after', {'changed': True}), ('owned_cleanup', []), + ('environment_after', {}), ('profile_before', {}), + ('engine_sha256_after', 'b'*64), ('spec_sha256', 'b'*64)]: + with self.subTest(field=field): + altered = copy.deepcopy(receipt) + altered[field] = value + with self.assertRaises(ValueError): + check.inspect_os_receipt(altered, source) + + +class SemanticTests(unittest.TestCase): + def setUp(self): + self.guest = dict(guest_id='g', state='QUEUING', current_cell=[1, 0, 0], + target_facility_id='f', current_service_id='', queue_request_id='q') + self.staff = dict(staff_id='s', state='WORKING', current_cell=[2, 0, 1], + current_task_id='t', assigned_facility_id='f', role_id='porter', + carried_inventory_id='cargo') + self.task = dict(task_id='t', state='WORKING', task_type='delivery', + assigned_staff_id='s', target_entity_id='request', rescue_facility_id='') + self.snapshot = {'systems': {'runtime': {'guests': {'guests': [self.guest]}, + 'staff': {'staff': [self.staff], 'tasks': [self.task]}}}} + self.report = dict(guests=[dict(id='g', state='QUEUING', cell=[1, 0, 0], target='f', service='', queue='q')], + staff=[dict(id='s', state='WORKING', cell=[2, 0, 1], task='t', facility='f', + role='porter', carried_inventory_id='cargo')], + tasks=[dict(id='t', state='WORKING', type='delivery', staff='s', + target='request', rescue_facility='')]) + + def test_real_schema_projection(self): + self.assertEqual(check.compare_actor_rows(self.snapshot, self.report), ['guests', 'staff', 'tasks']) + + def test_changed_state_position_target_task_and_goods_are_rejected(self): + for category, field, replacement in [('guests', 'cell', [8, 0, 0]), ('guests', 'target', 'other'), + ('guests', 'state', 'MOVING'), ('staff', 'task', ''), + ('staff', 'carried_inventory_id', ''), ('tasks', 'staff', 'other')]: + with self.subTest(category=category, field=field): + report = copy.deepcopy(self.report) + report[category][0][field] = replacement + with self.assertRaises(ValueError): + check.compare_actor_rows(self.snapshot, report) + + def test_missing_extra_and_boolean_only_rows_are_rejected(self): + for bad in [[], self.report['guests'] * 2, [True]]: + report = copy.deepcopy(self.report) + report['guests'] = bad + with self.assertRaises(ValueError): + check.compare_actor_rows(self.snapshot, report) + + def test_unknown_enum_and_duplicate_id_are_rejected(self): + self.guest['state'] = 'FICTIONAL' + with self.assertRaises(ValueError): + check.compare_actor_rows(self.snapshot, self.report) + self.guest['state'] = 'QUEUING' + self.snapshot['systems']['runtime']['guests']['guests'].append(copy.deepcopy(self.guest)) + with self.assertRaises(ValueError): + check.compare_actor_rows(self.snapshot, self.report) + + def test_typed_rectangle_and_duplicate_vector_key(self): + self.assertEqual(check.decode(dict(type='Rect2', text='Rect2(1, 2, 16, 24)')), [1, 2, 16, 24]) + with self.assertRaises(ValueError): + check.decode(dict(type='Rect2', text='Rect2(1, nan, 16, 24)')) + key = dict(type='Vector3i', text='Vector3i(1, 2, 0)') + value = dict(type='int', text='1') + with self.assertRaises(ValueError): + check.decode(dict(type='Dictionary', entries=[[key, value], [key, value]])) + + def test_immutable_typed_input_is_not_modified(self): + before = copy.deepcopy(self.snapshot) + check.compare_actor_rows(self.snapshot, self.report) + self.assertEqual(before, self.snapshot) + + +class PackingTests(unittest.TestCase): + def test_scale_trial_rebuilds_vertical_groups(self): + members = self.members(2) + for member, y in zip(members, (118, 144)): + member['row'].update(center=[108, y], at=[84, y-15]) + member['bounds'] = [80, 74, 20, 130] + # At p=3 the vertical intervals intersect; at p=2 they separate. + # Keeping the stale component would incorrectly force both down to p=1. + rows, badges = check.fitted_actor_layout(members, 4, 96) + self.assertEqual(badges, []) + self.assertEqual([rows['guest/'+str(i)]['scale'] for i in range(2)], [2, 2]) + self.assertEqual([rows['guest/'+str(i)]['at'] for i in range(2)], [[76, 120], [76, 146]]) + check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[])) + + def members(self, count=4): + # R13 cell dimensions and the actual _actor_offset sequence; no transit. + offsets = [0, 16, -16, 8, -8, 24, -24] + return [dict(row=dict(key='guest/'+str(i), center=[108+offsets[i % 7], 118-4*(i % 2)], + at=[84+offsets[i % 7], 103-4*(i % 2)], scale=3, + mode='idle', frame=0, flip=False, state=3, cell=[1, 1, 0]), + bounds=[80, 74, 56, 88], transit=False) for i in range(count)] + + def test_four_movers_fit_at_two_not_three(self): + rows, badges = check.fitted_actor_layout(self.members(), 4, 96) + self.assertEqual(badges, []) + self.assertEqual([rows['guest/'+str(i)]['scale'] for i in range(4)], [2]*4) + self.assertEqual([rows['guest/'+str(i)]['at'] for i in range(4)], + [[88, 120], [112, 116], [76, 120], [100, 116]]) + check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[])) + + def test_cross_kind_group_and_passenger(self): + members = self.members() + members[1]['row']['key'] = 'staff/worker' + members[2]['row']['key'] = 'guest/passenger' + members[2]['row']['scale'] = 2 + members[2]['row']['at'] = [76, 120] + rows, badges = check.fitted_actor_layout(members, 4, 96) + self.assertEqual(badges, []) + self.assertEqual(len(rows), 4) + check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[])) + + def test_saturation_keeps_all_identities(self): + rows, badges = check.fitted_actor_layout(self.members(12), 4, 96) + self.assertEqual(len(rows), 9) + self.assertEqual(len(badges), 1) + self.assertEqual(len(badges[0]['members']), 12) + self.assertEqual(badges[0]['text'], '+3') + self.assertEqual(set(badges[0]['drawn_keys']), set(rows)) + self.assertTrue(all(r['scale'] == 1 for r in rows.values())) + check.validate_actor_nonoverlap(dict(actor_drawings=list(rows.values()), cabins=[])) + + def test_each_actor_keeps_own_bounds(self): + members = self.members(2) + members[0]['bounds'] = [80, 74, 12, 88] + members[1]['bounds'] = [124, 74, 12, 88] + rows, badges = check.fitted_actor_layout(members, 4, 96) + self.assertEqual(badges, []) + self.assertEqual(check.actor_identity_core(rows['guest/0'])[0:3:2], [80, 12]) + self.assertEqual(check.actor_identity_core(rows['guest/1'])[0:3:2], [124, 12]) + + def test_receiving_cafeteria_uses_r13_room_not_access_cell(self): + fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/r13_service_region_gap.json').read_text()) + guest = fixture['guests'][0] + rect = fixture['room_rects'][guest['target']] + anchor = check.room_slots(rect, 4, 'cafeteria')[0] + center = [anchor[0], anchor[1]+12-48] + member = dict(row=dict(key='guest/'+guest['id'], center=center, + at=[center[0]-24, center[1]-15], scale=3, + mode='eat', frame=0, flip=False, state=5, cell=guest['cell']), + bounds=[rect[0]+4, rect[1]+4, rect[2]-8, rect[3]-8], transit=False) + rows, badges = check.fitted_actor_layout([member], 4, 96) + self.assertFalse(badges) + core = check.actor_identity_core(next(iter(rows.values()))) + self.assertGreaterEqual(core[0], 1031) + self.assertEqual(next(iter(rows.values()))['cell'], [5, 0, 0]) + + +class SpatialTests(unittest.TestCase): + def setUp(self): + self.runtime = dict(world=dict(bounds=dict(min_x=0, max_x=1, min_y=0, max_y=0), + walkable_cells=[[0, 0, 0], [1, 0, 0]], occupancies=[]), + path=dict(cell_traffic=[dict(cell=[0, 0, 0], base_capacity=2, + current_density=3, dynamic_cost=0, temporary_block=False)]), + facilities=dict(instances=[], requests=[]), guests=dict(guests=[]), + staff=dict(staff=[], tasks=[]), logistics_requests=[], + elevators=dict(current_minute=0, shafts=[], trips=[])) + self.snapshot = dict(systems=dict(runtime=self.runtime)) + self.report = dict(viewport=[184, 164], zoom=1, pan=[0, 0], layer=0, + traffic=[dict(cell=[0, 0, 0], rect=[78, 72, 12, 20], + band_rect=[79.44, 90.4, 9.12, 3.2], + traffic=dict(base_capacity=2, current_density=3, + dynamic_cost=0, temporary_block=False))], + queue_positions={}, queue_hidden={}, landing_badges=[], rooms={}, + treated_patients={}, carried_patients={}, resting_staff={}, + goods=dict(requests=[], deliveries=[]), actor_drawings=[], cabins=[], + observation_serial=1, overlay_consumed_serial=1, + overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2]) + synthetic_repair28_fields(self.report) + + def four_movers(self): + self.runtime['world'].update(bounds=dict(min_x=1, max_x=1, min_y=1, max_y=1), walkable_cells=[[1, 1, 0]]) + self.report.update(viewport=[216, 236], paused=True, animation_time=0, + actor_tracks={}, boarded={}, actor_aggregations=[]) + for i, (center, at) in enumerate(zip([[108,118], [124,114], [92,118], [116,114]], + [[88,120], [112,116], [76,120], [100,116]])): + key = 'guest/'+str(i) + self.runtime['guests']['guests'].append(dict(guest_id=str(i), current_cell=[1,1,0], + current_service_id='', target_facility_id='', state='MOVING')) + self.report['actor_tracks'][key] = dict(cell=[1,1,0], **{'from':[1,1,0]}, at=0, direction=1) + hashed = 5381 + for char in key: + hashed = (hashed*33+ord(char)) & 0xffffffff + self.report['actor_drawings'].append(dict(key=key, center=center, at=at, scale=2, + cell=[1,1,0], mode='idle', frame=hashed%15, variant=hashed%15, flip=False, + staff=False, state=3, receiving=False, carrying=False, riding=False)) + + def test_four_movers_complete_geometry(self): + self.four_movers() + check.compare_actor_geometry(self.snapshot, self.report) + + def moving_four(self): + self.four_movers() + self.report.update(paused=False, animation_time=.21, badges=[]) + for row in self.report['actor_drawings']: + self.report['actor_tracks'][row['key']]['from'] = [0, 1, 0] + row['center'][0] -= 32 + row.update(scale=3, mode='walk', frame=row['variant']+2) + row['at'] = [row['center'][0]-24, row['center'][1]-15] + + def drawing_audit(self): + rows = [] + for category, identifier, rect, source, text in check.drawing_rectangle_rows(self.report): + self.assertIsNone(text) + colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', + (check.ROOT/source).read_text()))) + rows.append(dict(category=category, id=identifier, rect=rect, + style_source=source, style_colors=colors)) + return dict(rows=rows) + + def test_transit_raw_state_in_full_and_standalone_paths(self): + self.moving_four() + check.compare_actor_geometry(self.snapshot, self.report) + check.validate_drawing_rows(self.report, self.drawing_audit(), state=self.snapshot) + # Without authoritative context the overlapping moving cores fail closed. + with self.assertRaisesRegex(ValueError, 'core overlap'): + check.validate_drawing_rows(self.report, self.drawing_audit()) + + def test_transit_cannot_be_claimed_by_flags_or_invalid_tracks(self): + self.moving_four() + mutations = { + 'paused': lambda r: r.update(paused=True), + 'expired': lambda r: r.update(animation_time=.42), + 'nonboolean_pause': lambda r: r.update(paused=0), + 'future': lambda r: r['actor_tracks']['guest/0'].update(at=1), + 'nonfinite': lambda r: r.update(animation_time=float('nan')), + 'wrong_endpoint': lambda r: r['actor_tracks']['guest/0'].update(cell=[0, 1, 0]), + 'teleport_origin': lambda r: r['actor_tracks']['guest/0'].update({'from': [-9, 1, 0]}), + } + for name, mutate in mutations.items(): + report = copy.deepcopy(self.report) + mutate(report) + for row in report['actor_drawings']: + row.update(in_transit=True, region='transit') + with self.subTest(name=name), self.assertRaises(ValueError): + check.validate_drawing_rows(report, self.drawing_audit(), state=self.snapshot) + + def test_staff_and_guests_share_complete_geometry_front_and_back(self): + self.four_movers() + self.runtime['guests']['guests'].pop() + self.runtime['staff']['staff'] = [dict(staff_id='3', current_cell=[1, 1, 0], + state='AVAILABLE', assigned_facility_id='', carried_inventory_id='', role_id='guard')] + self.report['actor_tracks']['staff/3'] = self.report['actor_tracks'].pop('guest/3') + row = self.report['actor_drawings'][-1] + hashed = 5381 + for char in 'staff/3': + hashed = (hashed*33+ord(char)) & 0xffffffff + row.update(key='staff/3', staff=True, state=2, variant=hashed%15, frame=hashed%15) + for layer in (0, 1): + self.report['layer'] = layer + for member in self.runtime['guests']['guests'] + self.runtime['staff']['staff']: + member['current_cell'][2] = layer + for track in self.report['actor_tracks'].values(): + track['cell'][2] = layer + track['from'][2] = layer + for drawing in self.report['actor_drawings']: + drawing['cell'][2] = layer + with self.subTest(layer=layer): + check.compare_actor_geometry(self.snapshot, self.report) + + def test_spoofed_region_does_not_allow_core_outside_own_cell(self): + self.four_movers() + self.report['actor_drawings'][0].update(region='facility', in_transit=True, at=[500, 120]) + with self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, self.report) + + def test_aggregation_badge_is_required_in_drawing_audit(self): + self.four_movers() + self.report['badges'] = [] + audit = self.drawing_audit() + self.report['actor_aggregations'] = [dict(at=[80, 74], text='+1', + members=[], drawn_keys=[])] + # This low-level test covers badge provenance only. Full geometry already + # rejects the unnecessary aggregate in test_fitted_geometry_rejects_tampering. + with self.assertRaisesRegex(ValueError, 'offline rectangle coverage'): + check.validate_drawing_rows(self.report, audit) + + def test_low_world_pixel_complete_geometry_keeps_fractional_base(self): + self.four_movers() + self.runtime['guests']['guests'] = self.runtime['guests']['guests'][:1] + self.report.update(viewport=[156, 146], actor_drawings=self.report['actor_drawings'][:1]) + self.report['actor_drawings'][0].update(center=[78, 73], at=[77, 72], scale=.1875) + check.compare_actor_geometry(self.snapshot, self.report) + + def test_saturated_complete_geometry_keeps_every_identity(self): + self.four_movers() + prototype = copy.deepcopy(self.report['actor_drawings'][0]) + self.runtime['guests']['guests'] = [] + self.report['actor_drawings'] = [] + # 10 upright cores need 60px at p=1, but the interval is only 56px. + # The first nine in preferred-x/key order occupy x82..136; guest/5 + # cannot be retained. This expected layout is independent of the solver. + order = [6, 2, 9, 4, 0, 7, 3, 1, 8, 5] + identities = {} + for i in range(10): + key = 'guest/'+str(i) + cell = [1, 1, 0] + self.runtime['guests']['guests'].append(dict(guest_id=str(i), current_cell=cell, + current_service_id='', target_facility_id='', state='MOVING')) + self.report['actor_tracks'][key] = dict(cell=cell, **{'from': cell}, at=0, direction=1) + identities[i] = dict(key=key, id=str(i), cell=cell, state=3, mode='idle') + if i == 5: + continue + hashed = 5381 + for char in key: + hashed = (hashed*33+ord(char)) & 0xffffffff + center = [108+[0,16,-16,8,-8,24,-24][i%7], 118-4*(i%2)] + self.report['actor_drawings'].append(dict(prototype, key=key, center=center, + at=[76+6*order.index(i), center[1]+19], scale=1, frame=hashed%15, variant=hashed%15)) + self.report['actor_aggregations'] = [dict(members=[identities[i] for i in order], + drawn_keys=['guest/'+str(i) for i in order[:-1]], at=[80,74], text='+1')] + check.compare_actor_geometry(self.snapshot, self.report) + self.report['actor_aggregations'][0]['members'].pop() + with self.assertRaisesRegex(ValueError, 'aggregation/no-fit'): + check.compare_actor_geometry(self.snapshot, self.report) + + def test_fitted_geometry_rejects_tampering(self): + self.four_movers() + for field, value in [('cell', [0,1,0]), ('state', 4), ('scale', 3), ('at', [999,120])]: + report = copy.deepcopy(self.report) + report['actor_drawings'][0][field] = value + with self.subTest(field=field), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, report) + for mutation in ('missing', 'duplicate', 'aggregate'): + report = copy.deepcopy(self.report) + if mutation == 'missing': + report['actor_drawings'].pop() + elif mutation == 'duplicate': + report['actor_drawings'].append(copy.deepcopy(report['actor_drawings'][0])) + else: + report['actor_aggregations'] = [dict(drawn_keys=[], text='+4')] + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, report) + + def test_density_and_pan_are_independently_recomputed(self): + check.compare_traffic(self.snapshot, self.report) + for key, value in [('current_density', 0), ('base_capacity', 9)]: + changed = copy.deepcopy(self.report) + changed['traffic'][0]['traffic'][key] = value + with self.assertRaises(ValueError): + check.compare_traffic(self.snapshot, changed) + self.report['pan'][0] = 20 + with self.assertRaises(ValueError): + check.compare_traffic(self.snapshot, self.report) + + def test_missing_queue_overflow_rejected(self): + saved = json.loads((check.ROOT / 'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved'] + self.runtime['elevators'] = saved + saved['trips'] = [dict(copy.deepcopy(saved['trips'][0]), actor_id='guest:g'+str(i), + request_id='trip'+str(i), enqueued_at=i) for i in range(3)] + self.report.update(shaft_footprint=[[0,0,0],[1,0,0],[0,1,0],[1,1,0]], queue_positions={}, + queue_hidden={'guest/g0': True, 'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54], + text='待ち3人(他3人)', cell=[0, 0, 0], + members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=0, overflow=3)]) + check.compare_queues(self.snapshot, self.report) + self.report['landing_badges'][0]['overflow'] = 0 + with self.assertRaises(ValueError): + check.compare_queues(self.snapshot, self.report) + + def test_fictional_goods_and_rooms_rejected(self): + check.compare_goods(self.snapshot, self.report) + check.compare_room_services(self.snapshot, self.report) + self.report['goods']['requests'].append(dict(stock=100)) + with self.assertRaises(ValueError): + check.compare_goods(self.snapshot, self.report) + self.report['rooms']['invented'] = dict(count=0) + with self.assertRaises(ValueError): + check.compare_room_services(self.snapshot, self.report) + + def test_room_reason_and_rescue_occupancy(self): + room = dict(instance_id='clinic', origin=[0, 0, 0], definition_id='infirmary', + state='NO_POWER', state_reasons=['NO_POWER'], suspension_reason='', + customer_capacity=2, active_staff_ids=[], active_services=[], occupied_cells=[]) + self.runtime['facilities']['instances'] = [room] + self.runtime['staff']['tasks'] = [dict(task_id='rescue', task_type='rescue_transport', + state='WORKING', assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='clinic')] + self.report['treated_patients'] = {'patient': 'clinic'} + self.report['rooms'] = {'clinic': dict(state=2, state_name='NO_POWER', reasons=['NO_POWER'], + operation=0, people=['patient'], count=1, limit=2, staff=[], suspension='')} + check.compare_room_services(self.snapshot, self.report) + for field, wrong in [('state', 6), ('operation', 1), ('people', []), ('count', 0)]: + changed = copy.deepcopy(self.report) + changed['rooms']['clinic'][field] = wrong + with self.assertRaises(ValueError): + check.compare_room_services(self.snapshot, changed) + + def test_cabin_geometry_and_phase(self): + shaft = dict(config=dict(shaft_id='lift', stops=[[0, 0, 0]], width=2, capacity=4), + current_floor=0, phase='IDLE', operational=True, boarded=[]) + self.runtime['elevators']['shafts'] = [shaft] + self.report.update(paused=True, animation_time=0, elevator_tracks={'lift': dict(floor=0, at=0)}, + cabins=[dict(shaft_id='lift', rect=[80, 72, 24, 19], pixel=1, operational=True, + doors_open=True, badge_at=[80, 58], badge='0/4人', passengers=[])]) + synthetic_repair28_fields(self.report, self.snapshot) + check.compare_cabin_geometry(self.snapshot, self.report) + for field, value in [('rect', [80, 70, 24, 19]), ('doors_open', False), ('badge', '4/4人')]: + changed = copy.deepcopy(self.report) + changed['cabins'][0][field] = value + with self.assertRaises(ValueError): + check.compare_cabin_geometry(self.snapshot, changed) + + +class AnchoredGeometryTests(unittest.TestCase): + """Whole state classification fixtures, with independently chosen draw rows.""" + def setUp(self): + SpatialTests.setUp(self) + self.runtime['world'].update(bounds=dict(min_x=0, max_x=3, min_y=0, max_y=1), + walkable_cells=[[x, y, 0] for y in range(2) for x in range(4)]) + self.report.update(viewport=[408, 332], paused=True, animation_time=0, + actor_tracks={}, boarded={}, actor_aggregations=[], badges=[], traffic=[]) + self.runtime.update(defense=dict(raids=[]), events=dict(actor_locks={})) + + def person(self, key, cell, state='MOVING', **fields): + kind, identifier = key.split('/') + if kind == 'guest': + person = dict(guest_id=identifier, current_cell=cell, state=state, + current_service_id='', target_facility_id='') + self.runtime['guests']['guests'].append(person) + else: + person = dict(staff_id=identifier, current_cell=cell, state=state, health=100, + assigned_facility_id='', current_task_id='', carried_inventory_id='', role_id='guard') + self.runtime['staff']['staff'].append(person) + person.update(fields) + self.report['actor_tracks'][key] = dict(cell=cell[:], **{'from': cell[:]}, at=0, direction=1) + return person + + def drawing(self, key, center, mode='idle', scale=3, receiving=False, riding=False, at=None): + kind, identifier = key.split('/') + source = self.runtime['staff']['staff'] if kind == 'staff' else self.runtime['guests']['guests'] + person = next(r for r in source if r[kind+'_id'] == identifier) + hashed = 5381 + for char in key: + hashed = (hashed*33+ord(char)) & 0xffffffff + row = dict(key=key, center=center, at=at or [center[0]-8*scale, center[1]+36-17*scale], + scale=scale, mode=mode, frame=hashed%15, variant=hashed%15, flip=False, + staff=kind == 'staff', state=check.enum_names(check.ROOT, + 'scripts/'+kind+'/'+kind+'_runtime.gd', 'State').index(person['state']), + cell=person['current_cell'][:], receiving=receiving, carrying=False, riding=riding) + if not riding: + self.report['actor_drawings'].append(row) + return row + + def room(self, fid, definition, cells, users=(), workers=()): + facility = dict(instance_id=fid, definition_id=definition, origin=cells[0], occupied_cells=cells, + state='OPEN', state_reasons=[], suspension_reason='', customer_capacity=10, + active_services=[dict(guest_ids=list(users))] if users else [], active_staff_ids=list(workers), queue_entries=[]) + self.runtime['facilities']['instances'].append(facility) + self.runtime['world']['occupancies'].append(dict(entity_id=fid, cells=cells)) + self.report['rooms'][fid] = dict(people=list(users), count=len(users), staff=list(workers), + state=check.enum_names(check.ROOT, 'scripts/facility/facility_instance.gd', 'State').index('OPEN'), + state_name='OPEN', reasons=[], operation=1, limit=10, suspension='', definition_id=definition, + rect=check.Transform(self.runtime, self.report).room(cells)) + return facility + + def queue_work(self): + facility = self.room('work', 'cafeteria', [[x,y,0] for y in (0,1) for x in (2,3)], workers=['w']) + for key in ('a', 'b'): + self.person('guest/'+key, [1,1,0], 'QUEUING', current_service_id='service', target_facility_id='work') + # Queue takes precedence even over a nonempty current_service_id. + facility['active_services'] = [dict(guest_ids=['a','b'])] + self.report['rooms']['work'].update(people=['a','b'], count=2) + facility['queue_entries'] = ['request'] + self.runtime['facilities']['requests'] = [dict(request_id='request', guest_ids=['a','b'])] + self.person('staff/w', [2,1,0], 'WORKING', assigned_facility_id='work') + self.report['queue_positions'] = {'guest/a':[172,118], 'guest/b':[108,118]} + self.report['landing_badges'] = [dict(at=[140,54], text='待ち2人', cell=[1,1,0], + members=['guest/a','guest/b'], visible_count=2, overflow=0)] + self.drawing('guest/a', [172,118], 'eat', receiving=True) + self.drawing('guest/b', [108,118], 'eat', receiving=True) + self.drawing('staff/w', [236,126], 'work') + + def test_queue_lane_and_work_staff(self): + self.queue_work() + check.compare_actor_geometry(self.snapshot, self.report) + + def test_queue_precedence_order_and_category_forgeries(self): + self.queue_work() + for mutation in ('precedence', 'order', 'category', 'feet', 'pose'): + report = copy.deepcopy(self.report) + if mutation == 'precedence': report['actor_drawings'][0].update(center=[268,210], at=[244,195]) + if mutation == 'order': + report['queue_positions']['guest/a'], report['queue_positions']['guest/b'] = report['queue_positions']['guest/b'], report['queue_positions']['guest/a'] + if mutation == 'category': report['actor_drawings'][0].update(staff=True, region='work', in_transit=True) + if mutation == 'feet': report['actor_drawings'][0]['at'][1] -= 17 + if mutation == 'pose': report['actor_drawings'][0]['mode'] = 'wait' + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, report) + + def test_receiving_r13_guest_cannot_be_forced_into_access_cell(self): + fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/r13_service_region_gap.json').read_text()) + guest = fixture['guests'][0] + self.runtime['world']['bounds'].update(max_x=9) + self.report.update(viewport=[792,332], pan=[567,318]) + fid = guest['target'] + self.room(fid, 'cafeteria', [[x,y,0] for y in (0,1) for x in range(6,10)], users=[guest['id']]) + self.person('guest/'+guest['id'], guest['cell'], 'RECEIVING_SERVICE', current_service_id='service', target_facility_id=fid) + # Exact historical room x1027..1283; access cell x963..1027. + self.assertEqual(self.report['rooms'][fid]['rect'], fixture['room_rects'][fid]) + row = self.drawing('guest/'+guest['id'], [1069+2/3,528], 'eat', receiving=True, at=[1046,513]) + check.compare_actor_geometry(self.snapshot, self.report) + row.update(center=[995,532], at=[971,517], region='floor') + with self.assertRaises(ValueError): check.compare_actor_geometry(self.snapshot, self.report) + + def test_carried_patient_and_treated_staff(self): + self.person('staff/carrier', [0,0,0], 'AVAILABLE') + self.person('staff/patient', [0,0,0], 'INCAPACITATED') + self.person('staff/treated', [2,0,0], 'INCAPACITATED') + self.room('clinic', 'infirmary', [[2,0,0]]) + self.runtime['staff']['tasks'] = [dict(task_type='rescue_transport', state=phase, + assigned_staff_id='carrier', target_entity_id=patient, rescue_facility_id='clinic') + for patient, phase in [('patient','MOVING'), ('treated','WORKING')]] + self.report.update(carried_patients={'patient':'carrier'}, treated_patients={'treated':'clinic'}) + self.report['rooms']['clinic'].update(people=['treated'], count=1) + self.drawing('staff/carrier', [108,214]) + self.drawing('staff/patient', [108,166], 'collapsed') + row = self.drawing('staff/treated', [236,190], 'collapsed') + check.compare_actor_geometry(self.snapshot, self.report) + self.report['actor_drawings'][1]['center'][1] += 4 + self.report['actor_drawings'][1]['at'][1] += 4 + with self.assertRaises(ValueError): check.compare_actor_geometry(self.snapshot, self.report) + + def cabin(self, count): + shaft = json.loads((check.ROOT/'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved']['shafts'][0] + shaft.update(current_floor=0, phase='IDLE', boarded=['guest:p%02d'%i for i in range(count)]) + # Synthetic saved configuration, not a production definition or capacity edit. + shaft['config']['capacity'] = max(4, count) + self.runtime['elevators']['shafts'] = [shaft] + sid = shaft['config']['shaft_id'] + self.report['elevator_tracks'] = {sid: dict(floor=0, **{'from':0}, at=0)} + cabin = dict(shaft_id=sid, rect=[92,174,96,76], pixel=4, operational=True, doors_open=True, + badge_at=[92,160], badge=('満員 ' if count >= 4 else '')+f'{count}/{max(4,count)}人', passengers=[]) + self.report['cabins'] = [cabin] + synthetic_repair28_fields(self.report, self.snapshot) + for i in range(count): + key = 'guest/p%02d'%i + self.person(key, [0,0,0]) + self.report['boarded'][key] = True + cabin['passengers'].append(self.drawing(key, [116+32*(i%2),206-36*((i//2)%2)], scale=2, riding=True)) + return cabin + + def occupied_rest(self): + state, _, audit = RestAuditTests().fixture() + old = state['systems']['runtime'] + self.runtime['path'] = old['path'] + self.person('staff/s', [0,0,0], 'OFF_DUTY') + self.room('a', 'staff_waiting_room', [[0,0,0]]) + self.room('b', 'staff_waiting_room', [[0,0,0]]) + for facility in self.runtime['facilities']['instances']: + facility['occupied_cells'] = [] # Public selector falls back to origin. + self.report['resting_staff'] = {'s':'a'} + self.report['rooms']['a'].update(people=['s'], count=1) + self.drawing('staff/s', [108,198]) + return audit + + def test_compare_rest_with_actual_occupied_facility(self): + audit = self.occupied_rest() + self.assertTrue(check.compare_rest(self.snapshot, self.report, audit)['s']['resting']) + self.report['actor_drawings'][0]['at'][1] += 1 + with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit) + + def test_compare_rest_independently_proves_no_fit(self): + audit = self.occupied_rest() + rest = self.report['actor_drawings'].pop() + rest.update(scale=1, at=[100,217]) + order = [6,2,9,4,0,7,3,1,8,5] + identities = {} + for i in range(10): + key = 'guest/'+str(i) + self.person(key, [0,0,0]) + center = [108+[0,16,-16,8,-8,24,-24][i%7],214-4*(i%2)] + row = self.drawing(key, center, scale=1, at=[76+6*order.index(i),center[1]+19]) + identities[i] = dict(key=key, id=str(i), state=3, cell=[0,0,0], mode='idle') + self.report['actor_drawings'] = [r for r in self.report['actor_drawings'] if r['key'] != 'guest/5']+[rest] + self.report['actor_aggregations'] = [dict(members=[identities[i] for i in order], + drawn_keys=['guest/'+str(i) for i in order[:9]], at=[80,170], text='+1')] + check.compare_rest(self.snapshot, self.report, audit) + stale = copy.deepcopy(self.report) + stale['actor_aggregations'][0]['members'].append(dict(key='staff/s', id='s', state=0, cell=[0,0,0], mode='idle')) + with self.assertRaises(ValueError): check.compare_rest(self.snapshot, stale, audit) + self.report['actor_aggregations'][0]['members'].pop() + with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit) + + def test_compare_rest_with_queued_staff(self): + audit = self.occupied_rest() + self.person('staff/q', [0,0,0], 'OFF_DUTY') + saved = json.loads((check.ROOT/'tests/fixtures/p23-03/queued_elevator.json').read_text())['saved'] + saved['trips'][0]['actor_id'] = 'staff:q' + self.runtime['elevators'] = saved + self.runtime['world']['walkable_cells'].append([-1,0,0]) + self.report['queue_positions'] = {'staff/q':[44,214]} + self.report['landing_badges'] = [dict(at=[-4,150], text='待ち1人', cell=[0,0,0], + members=['staff/q'], visible_count=1, overflow=0)] + queued = copy.deepcopy(audit['staff'][0]) + queued.update(staff_id='q', in_transit=True) + queued['comparison_only'].update(resting=False, label='休憩先へ移動中') + audit['staff'].append(queued) + self.drawing('staff/q', [44,214], 'wait') + synthetic_repair28_fields(self.report, self.snapshot) + result = check.compare_rest(self.snapshot, self.report, audit) + self.assertTrue(result['s']['resting']) + self.assertFalse(result['q']['resting']) + self.report['actor_drawings'][-1].update(center=[108,198], at=[84,183]) + with self.assertRaises(ValueError): check.compare_rest(self.snapshot, self.report, audit) + + def test_passenger_and_floor_actor_front_and_back(self): + cabin = self.cabin(1) + self.person('guest/floor', [2,0,0]) + self.drawing('guest/floor', [236,214]) + for layer in (0,1): + self.report['layer'] = layer + for guest in self.runtime['guests']['guests']: guest['current_cell'][2] = layer + for cell in self.runtime['elevators']['shafts'][0]['config']['stops']: cell[2] = layer + for row in self.report['actor_drawings']+cabin['passengers']: row['cell'][2] = layer + for track in self.report['actor_tracks'].values(): + track['cell'][2] = track['from'][2] = layer + synthetic_repair28_fields(self.report, self.snapshot) + with self.subTest(layer=layer): + check.compare_cabin_geometry(self.snapshot, self.report) + check.compare_actor_geometry(self.snapshot, self.report) + + def test_cabin_aggregation_retains_full_boarded_membership(self): + cabin = self.cabin(40) + originals = copy.deepcopy(cabin['passengers']) + retained = {} + for y in (170,206): + group = sorted([r for r in originals if r['center'][1] == y], key=lambda r:(r['center'][0],r['key'])) + drawn = group[:14] # 88px holds fourteen 6px cores, never fifteen. + for i, row in enumerate(drawn): + row.update(scale=1, at=[94+6*i,y+19]) + retained[row['key']] = row + self.report['actor_aggregations'].append(dict( + members=[dict(key=r['key'], id=r['key'][6:], state=r['state'], cell=r['cell'], mode=r['mode']) for r in group], + drawn_keys=[r['key'] for r in drawn], at=[96,178], text='+6')) + cabin['passengers'] = [retained[r['key']] for r in originals if r['key'] in retained] + check.compare_cabin_geometry(self.snapshot, self.report) + check.compare_actor_geometry(self.snapshot, self.report) + self.assertEqual(sum(len(a['members']) for a in self.report['actor_aggregations']), 40) + for mutation in ('lost_identity','badge'): + report = copy.deepcopy(self.report) + if mutation == 'lost_identity': report['actor_aggregations'][0]['members'].pop() + else: report['actor_aggregations'][0]['at'][0] += 1 + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, report) + + +class CabinLandingTests(unittest.TestCase): + """Constructed raw topology; explicit expected draws, never OS evidence.""" + setUp = AnchoredGeometryTests.setUp + person = AnchoredGeometryTests.person + drawing = AnchoredGeometryTests.drawing + + def lift(self, floor=0): + self.runtime['elevators']['shafts'] = [dict( + config=dict(shaft_id='lift', stops=[[1,0,0], [1,1,0]], width=2, capacity=4), + current_floor=0, phase='PICKUP', boarding_floor=1, phase_origin_floor=0, + remaining_minutes=10*(1-floor), phase_total_minutes=10, operational=True, boarded=[])] + self.report['elevator_tracks'] = {'lift': dict(floor=floor, **{'from': floor}, at=0)} + # The transform is 64x96; the full two-cell cabin is 96x76. + y = check.math.copysign(check.math.floor(abs(174-96*floor)+.5), 174-96*floor) + self.report['cabins'] = [dict(shaft_id='lift', rect=[156,y,96,76], pixel=4, + operational=True, doors_open=False, badge_at=[156,y-14], passengers=[])] + synthetic_repair28_fields(self.report, self.snapshot) + + def test_moving_guest_same_floor_and_old_position_negative(self): + self.lift() + self.person('guest/g', [1,0,0]) + self.drawing('guest/g', [108,214]) + check.compare_actor_geometry(self.snapshot, self.report) + bad = copy.deepcopy(self.report) + bad['actor_drawings'][0].update(center=[172,214], at=[148,199]) + with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin'): + check.compare_actor_geometry(self.snapshot, bad) + + def test_transit_from_lift_to_corridor_is_relocated_before_jitter(self): + self.lift() + self.person('guest/g', [0,0,0]) + self.report.update(paused=False, animation_time=.21) + self.report['actor_tracks']['guest/g']['from'] = [1,0,0] + row = self.drawing('guest/g', [108,214], 'walk') + row['frame'] += 2 + check.compare_actor_geometry(self.snapshot, self.report) + bad = copy.deepcopy(self.report) + bad['actor_drawings'][0]['center'][0] += 1 + with self.assertRaisesRegex(ValueError, 'actor fitted center'): + check.compare_actor_geometry(self.snapshot, bad) + + def test_carrier_and_raised_patient_follow_relocated_region(self): + self.lift() + self.person('staff/carrier', [1,0,0], 'AVAILABLE') + self.person('staff/patient', [1,0,0], 'INCAPACITATED') + self.runtime['staff']['tasks'] = [dict(task_type='rescue_transport', state='MOVING', + assigned_staff_id='carrier', target_entity_id='patient', rescue_facility_id='')] + self.report['carried_patients'] = {'patient': 'carrier'} + self.drawing('staff/carrier', [108,214]) + self.drawing('staff/patient', [108,166], 'collapsed') + check.compare_actor_geometry(self.snapshot, self.report) + self.assertTrue(all(not check.intersects(check.actor_identity_core(r), self.report['cabins'][0]['rect']) + for r in self.report['actor_drawings'])) + + def test_fractional_moving_cabin_overlaps_floor_above(self): + self.lift(.8) + self.person('guest/g', [1,1,0]) + self.drawing('guest/g', [108,118]) + check.compare_actor_geometry(self.snapshot, self.report) + + def test_no_lane_aggregates_identity_and_never_drops_silently(self): + self.lift() + self.runtime['world']['walkable_cells'] = [[1,0,0], [2,0,0]] + self.person('guest/g', [1,0,0]) + self.report['actor_aggregations'] = [dict(members=[dict(key='guest/g', id='g', state=3, + cell=[1,0,0], mode='idle')], drawn_keys=[], at=[60,150], text='+1')] + check.compare_actor_geometry(self.snapshot, self.report) + for mutation in ('badge', 'identity', 'hidden'): + bad = copy.deepcopy(self.report) + if mutation == 'badge': bad['actor_aggregations'][0]['at'][0] += 1 + if mutation == 'identity': bad['actor_aggregations'][0]['members'] = [] + if mutation == 'hidden': + bad['actor_aggregations'] = [] + bad['queue_hidden'] = {'guest/g': True} + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, bad) + self.person('guest/h', [1,0,0]) + aggregate = self.report['actor_aggregations'][0] + aggregate['members'].append(dict(key='guest/h', id='h', state=3, cell=[1,0,0], mode='idle')) + aggregate['text'] = '+2' + check.compare_actor_geometry(self.snapshot, self.report) + + def test_reserved_lane_prefers_free_then_shares_with_queue(self): + self.lift() + self.person('guest/q', [1,0,0], 'QUEUING') + self.person('guest/g', [1,0,0]) + self.runtime['elevators']['trips'] = [dict(request_id='queue', actor_id='guest:q', state='QUEUED', + shaft_id='lift', connector_id='lift.link.0.1', enqueued_at=0, + **{'from': [1,0,0], 'to': [1,1,0]})] + self.report.update(queue_positions={'guest/q':[108,214]}, + landing_badges=[dict(at=[60,150], text='待ち1人', cell=[1,0,0], + members=['guest/q'], visible_count=1, overflow=0)]) + self.drawing('guest/q', [108,214], 'wait') + self.drawing('guest/g', [300,214]) + check.compare_actor_geometry(self.snapshot, self.report) + self.runtime['world']['walkable_cells'].remove([3,0,0]) + self.report['actor_drawings'][0].update(at=[100,199]) + self.report['actor_drawings'][1].update(center=[108,214], at=[82,199]) + check.compare_actor_geometry(self.snapshot, self.report) + + def test_flags_drop_and_unnecessary_relocation_fail(self): + self.lift() + self.person('guest/g', [1,0,0]) + self.drawing('guest/g', [108,214]) + check.compare_actor_geometry(self.snapshot, self.report) + for mutation in ('riding', 'region', 'boarded', 'drop'): + bad = copy.deepcopy(self.report) + if mutation == 'riding': bad['actor_drawings'][0]['riding'] = True + if mutation == 'region': + bad['actor_drawings'][0]['region'] = 'landing' + if mutation == 'boarded': bad['boarded']['guest/g'] = True + if mutation == 'drop': bad['actor_drawings'] = [] + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + check.compare_actor_geometry(self.snapshot, bad) + # A free corridor actor must remain at its actual cell, even beside a lift. + self.runtime['guests']['guests'][0]['current_cell'] = [3,0,0] + self.report['actor_tracks']['guest/g'].update(cell=[3,0,0], **{'from': [3,0,0]}) + with self.assertRaisesRegex(ValueError, 'actor fitted'): + check.compare_actor_geometry(self.snapshot, self.report) + + +class ComparisonTests(unittest.TestCase): + def pair(self): + declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort', + arrivals=[dict(id='g', offset=0)], window=2, to=[2, 1, 0], + improvement=dict(definition_id='guest_elevator', origin=[4, 0, 0])) + a = dict(declaration=declaration, initial=dict(seed=1)) + a['arrival-g'] = dict(offset=0, payload=dict(guest_id='g'), result=dict(code='OK')) + for minute in range(3): + a['tick-%03d' % minute] = dict(minute=minute, guests=dict(guests=[dict(guest_id='g', + current_cell=[2, 1, 0] if minute == 2 else [2, 0, 0])])) + b = copy.deepcopy(a) + b['build-0'] = dict(payload=declaration['improvement'], result=dict(code='OK')) + b['tick-001']['guests']['guests'][0]['current_cell'] = [2, 1, 0] + return a, b + + def test_strict_metric_from_typed_cells(self): + a, b = self.pair() + self.assertEqual(check.compare_layouts(a, b)['B'], 1) + for mutation in ('demand', 'incomplete', 'no_improvement', 'extra_build'): + with self.subTest(mutation=mutation): + a, b = self.pair() + if mutation == 'demand': + b['arrival-g']['payload']['guest_id'] = 'other' + elif mutation == 'incomplete': + for minute in (1, 2): + b['tick-%03d' % minute]['guests']['guests'][0]['current_cell'] = [2, 0, 0] + elif mutation == 'no_improvement': + b['tick-001'] = copy.deepcopy(a['tick-001']) + else: + b['build-1'] = copy.deepcopy(b['build-0']) + with self.assertRaises(ValueError): + check.compare_layouts(a, b) + + +class OfflineGeometryTests(unittest.TestCase): + """Synthetic calibration examples; no OS or acceptance credit.""" + + def setUp(self): + self.calibration = dict(window_size=[100, 80], window_position=[240, 150], + screenshot_pixels=[200, 190], backing_scale=2, + canvas_stretch=1, titlebar_pixels=30) + self.request = dict(window_size=[100, 80], window_position=[240, 150], + screen_scale=2, content_scale_factor=1, target_pid=123) + self.clicks = [dict(target_pid=123, events=[dict(action='click', + coordinates=[20, 70], calibration=copy.deepcopy(self.calibration), + point=dict(screen=[10, 20], viewport=[10, 20], + viewport_to_screen=[1, 0, 0, 1, 0, 0]))])] + + def transform(self): + return check.screenshot_transform(self.calibration, self.request, [200, 190], self.clicks) + + def test_uses_measured_scale_origin_not_desktop_position(self): + self.assertEqual(check.affine_rect(self.transform(), [10, 20, 5, 7]), [20, 70, 10, 14]) + + def test_shifted_titlebar_fails_independent_click(self): + self.calibration['titlebar_pixels'] += 1 + self.calibration['screenshot_pixels'][1] += 1 + self.clicks[0]['events'][0]['calibration'] = copy.deepcopy(self.calibration) + with self.assertRaises(ValueError): + check.screenshot_transform(self.calibration, self.request, [200, 191], self.clicks) + + def test_wrong_click_offset_fails(self): + self.clicks[0]['events'][0]['coordinates'][1] -= 30 + with self.assertRaisesRegex(ValueError, 'click screenshot offset'): + self.transform() + + def test_wrong_pid_cannot_calibrate(self): + self.clicks[0]['target_pid'] += 1 + with self.assertRaisesRegex(ValueError, 'missing independent click'): + self.transform() + + def test_nonfinite_and_false_scale_rejected(self): + for scale in (False, float('nan'), 0, -1): + self.calibration['backing_scale'] = scale + with self.assertRaisesRegex(ValueError, 'invalid screenshot calibration'): + self.transform() + + def test_all_visibility_cases_are_accounted_for(self): + self.assertEqual(check.rectangle_eligibility([2, 3, 4, 5], [10, 10]), + ('visible', [2, 3, 4, 5])) + self.assertEqual(check.rectangle_eligibility([-2, 3, 4, 5], [10, 10]), + ('clipped', [0, 3, 2, 5])) + self.assertEqual(check.rectangle_eligibility([10, 3, 4, 5], [10, 10])[0], 'outside') + self.assertEqual(check.rectangle_eligibility([2, 3, 0, 5], [10, 10])[0], 'outside') + + def test_invalid_rectangles_rejected(self): + for rect in ([0, 0, -1, 4], [float('nan'), 0, 1, 4]): + with self.assertRaisesRegex(ValueError, 'invalid clipping rectangle'): + check.rectangle_eligibility(rect, [10, 10]) + + def test_noise_is_measured_and_does_not_assert_class(self): + before = (2, 2, [[(10, 20, 30)]*2 for _ in range(2)]) + after = copy.deepcopy(before) + after[2][1][1] = (10, 27, 30) + noise = check.paired_crop_noise(before, after, [0, 0, 2, 2]) + self.assertEqual(noise, dict(samples=4, max_channel_delta=7, + mean_channel_delta=1.75, unchanged=3)) + self.assertNotIn('pass', noise) + self.assertNotIn('state', noise) + + def test_wrong_dimensions_and_empty_crop_rejected(self): + image = (1, 1, [[(0, 0, 0)]]) + with self.assertRaisesRegex(ValueError, 'paired image dimensions'): + check.paired_crop_noise(image, (2, 1, []), [0, 0, 1, 1]) + with self.assertRaisesRegex(ValueError, 'empty noise crop'): + check.paired_crop_noise(image, image, [1, 0, 1, 1]) + + def test_report_positions_and_source_provenance_are_recomputed(self): + source = 'scripts/ui/pixel_people.gd' + report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[], + actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, + mode='idle', frame=0, flip=False)]) + colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', + (check.ROOT/source).read_text()))) + audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18], + style_source=source, style_colors=colors)]) + check.validate_drawing_rows(report, audit) + for change in ('shift', 'identity', 'missing', 'palette'): + tampered = copy.deepcopy(audit) + if change == 'shift': tampered['rows'][0]['rect'][0] += 1 + if change == 'identity': tampered['rows'][0]['category'] = 'guest' + if change == 'missing': tampered['rows'] = [] + if change == 'palette': tampered['rows'][0]['style_colors'] = ['000000'] + with self.subTest(change=change), self.assertRaises(ValueError): + check.validate_drawing_rows(report, tampered) + + +class FrameReviewTests(unittest.TestCase): + def test_frame_raw_context_valid_malformed_and_missing_rest(self): + fixture = SpatialTests() + fixture.setUp() + fixture.moving_four() + fixture.report.update(viewport=[216,236], traffic=[]) + audit = fixture.drawing_audit() + for row in audit['rows']: + row.update(viewport_rect=row['rect'][:], screen_rect=row['rect'][:]) + for context, rest, passes in ((fixture.snapshot,None,True), ({},None,False)): + result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration), + [self.image], self.clicks, state=context, rest_audit=rest) + self.assertEqual(not result['errors'], passes) + fixture.report['resting_staff'] = {'s':'a'} + for rest in (None, [], {}): + result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration), + [self.image], self.clicks, state=fixture.snapshot, rest_audit=rest) + self.assertFalse(result['mechanical_pass']) + fixture.report['resting_staff'] = {} + result = check.frame_rows(fixture.report, audit, self.request, dict(calibration=self.calibration), + [self.image], self.clicks) + self.assertFalse(result['mechanical_pass']) + self.assertIn('core overlap', str(result['errors'])) + + def test_frame_with_valid_occupied_rest_authority(self): + fixture = AnchoredGeometryTests() + fixture.setUp() + rest = fixture.occupied_rest() + rows = [] + for category, identifier, rect, source, text in check.drawing_rectangle_rows(fixture.report): + self.assertIsNone(text) + colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', (check.ROOT/source).read_text()))) + rows.append(dict(category=category, id=identifier, rect=rect, viewport_rect=rect[:], + screen_rect=rect[:], style_source=source, style_colors=colors)) + result = check.frame_rows(fixture.report, dict(rows=rows), self.request, dict(calibration=self.calibration), + [self.image], self.clicks, state=fixture.snapshot, rest_audit=rest) + self.assertEqual(result['errors'], []) + rest['path_service_sha256'] = '0'*64 + result = check.frame_rows(fixture.report, dict(rows=rows), self.request, dict(calibration=self.calibration), + [self.image], self.clicks, state=fixture.snapshot, rest_audit=rest) + self.assertFalse(result['mechanical_pass']) + self.assertIn('rest PathService source pin', result['errors']) + + def test_os_offline_raw_context_binding_and_hashes(self): + def typed(value): + if isinstance(value, dict): return dict(type='Dictionary', entries=[[typed(k),typed(v)] for k,v in value.items()]) + if isinstance(value, list): return dict(type='Array', items=[typed(v) for v in value]) + return dict(type={str:'String',int:'int'}[type(value)], text=json.dumps(value)) + with tempfile.TemporaryDirectory() as temporary: + stream = Path(temporary) + (stream/'objects').mkdir() + def put(value): + raw = json.dumps(typed(value)).encode() + digest = hashlib.sha256(raw).hexdigest() + (stream/'objects'/(digest+'.json')).write_bytes(raw) + return digest + report = {'actor_drawings':[]} + state = {'systems':{'runtime':{}}} + rest = {'staff':[], 'path_service_sha256':check.sha(check.ROOT/'scripts/world/path_service.gd')} + capture = dict(report=put(report), state=put(state), rest_audit=put(rest)) + self.assertEqual(check.drawing_context(stream, capture, report), (state,rest)) + for field in ('state','rest_audit','report'): + missing = dict(capture) + del missing[field] + with self.assertRaises(ValueError): check.drawing_context(stream, missing, report) + for field in ('state','rest_audit'): + bad = dict(capture, **{field:put({})}) + with self.assertRaises(ValueError): check.drawing_context(stream, bad, report) + with self.assertRaises(ValueError): check.drawing_context(stream, capture, {'wrong':1}) + with self.assertRaises(FileNotFoundError): + check.drawing_context(stream, dict(capture, state='0'*64), report) + (stream/'objects'/(capture['state']+'.json')).write_text('{}') + with self.assertRaisesRegex(ValueError, 'object hash'): check.drawing_context(stream, capture, report) + + def setUp(self): + OfflineGeometryTests.setUp(self) + self.request.update(local_to_viewport=[1, 0, 0, 1, 0, 0], + viewport_to_screen=[1, 0, 0, 1, 0, 0], + local_to_screen=[1, 0, 0, 1, 0, 0]) + self.report = dict(layer=0, viewport=[100, 80], rooms={}, cabins=[], + landing_badges=[], badges=[], resting_staff={}, traffic=[], + actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1, state=0, + mode='idle', frame=0, flip=False)]) + source = 'scripts/ui/pixel_people.gd' + colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', + (check.ROOT/source).read_text()))) + self.audit = dict(rows=[dict(category='staff', id='staff/s', rect=[1, 1, 16, 18], + viewport_rect=[1, 1, 16, 18], screen_rect=[1, 1, 16, 18], + style_source=source, style_colors=colors)]) + self.image = (200, 190, [[(x % 256, y % 256, 0) for x in range(200)] for y in range(190)]) + + def frame(self): + return check.frame_rows(self.report, self.audit, self.request, + dict(calibration=self.calibration), [self.image], self.clicks) + + def test_frame_without_raw_state_explicitly_denies_transit_exemptions(self): + self.report['actor_drawings'][0]['in_transit'] = True + self.assertEqual(self.frame()['transit_authority'], + 'absent: fail closed, no transit exemptions') + + def test_blank_crop_fails_without_threshold(self): + self.image = (200, 190, [[(0, 0, 0)] * 200 for _ in range(190)]) + result = self.frame() + self.assertFalse(result['mechanical_pass']) + self.assertIn('blank crop', result['rows'][0]['failures']) + + def test_missing_extra_and_unaudited_traffic_fail(self): + for change in ('missing', 'extra', 'traffic'): + with self.subTest(change=change): + original = copy.deepcopy(self.audit) + if change == 'missing': self.audit['rows'].clear() + if change == 'extra': self.audit['rows'] *= 2 + if change == 'traffic': + self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5], + traffic=dict(current_density=1, temporary_block=False))] + result = self.frame() + self.assertFalse(result['mechanical_pass']) + self.assertEqual(len(result['rows']), len(self.audit['rows'])) + self.assertIn('offline rectangle coverage', result['errors']) + self.audit = original + + def test_dense_or_blocked_traffic_row_closes_audit_gap(self): + source = 'scenes/main/world_preview.gd' + colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', + (check.ROOT/source).read_text()))) + for density, blocked in ((1, False), (0, True), (0, False)): + with self.subTest(density=density, blocked=blocked): + self.report['traffic'] = [dict(cell=[-6, 2, 0], rect=[1, 1, 5, 5], + traffic=dict(current_density=density, temporary_block=blocked))] + active = density > 0 or blocked + missing = self.frame() + self.assertEqual(missing['mechanical_pass'], not active) + if active: + self.assertIn('offline rectangle coverage', missing['errors']) + self.audit['rows'].append(dict(category='traffic', id='-6,2,0', + rect=[1, 1, 5, 5], viewport_rect=[1, 1, 5, 5], + screen_rect=[1, 1, 5, 5], style_source=source, style_colors=colors)) + complete = self.frame() + self.assertTrue(complete['mechanical_pass'], complete['errors']) + self.audit['rows'].pop() + + def test_wrong_state_enum_and_name_fail(self): + source = 'scripts/ui/pixel_tower_art.gd' + self.report['actor_drawings'] = [] + room = dict(rect=[1, 1, 16, 18], state=3, state_name='NO_STAFF', operation=0) + self.report['rooms'] = {'r': room} + self.audit['rows'][0].update(category='room', id='r', style_source=source, + style_colors=list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', + (check.ROOT/source).read_text())))) + self.assertEqual(self.frame()['rows'][0]['operation'], 'INACTIVE') + for state, name in ((99, 'NO_STAFF'), (3, 'OPEN'), (True, 'NO_ACCESS')): + room.update(state=state, state_name=name) + with self.assertRaises(ValueError): self.frame() + + def test_frame_rejects_affine_and_calibration_shift(self): + self.request['local_to_screen'][4] += 1 + with self.assertRaisesRegex(ValueError, 'affine composition'): self.frame() + self.request['local_to_screen'][4] -= 1 + self.clicks[0]['events'][0]['coordinates'][1] += 1 + with self.assertRaisesRegex(ValueError, 'click screenshot offset'): self.frame() + + def test_eligible_crop_fully_outside_image_fails(self): + for key in ('viewport_to_screen', 'local_to_screen'): + self.request[key][4] = 300 + self.audit['rows'][0]['screen_rect'][0] += 300 + result = self.frame() + self.assertFalse(result['mechanical_pass']) + self.assertIn('eligible crop outside image', result['rows'][0]['failures']) + + def test_manifest_crop_and_independent_review_binding(self): + with tempfile.TemporaryDirectory() as temporary: + directory = Path(temporary) + manifest, digest = check.write_frame_review(directory, self.frame(), + [dict(whole_image_sha256='a'*64, raw_sha256='b'*64, conversion_sha256='c'*64)]) + self.assertEqual(check.sha(directory/'manifest.json'), digest) + crop = manifest['rows'][0]['crops'][0] + self.assertEqual(check.sha(directory/crop['path']), crop['sha256']) + self.assertEqual(check.png_pixels((directory/crop['path']).read_bytes())[:2], (32, 36)) + verdict_path = directory/'independent-verdict.json' + self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set()) + verdict = dict(manifest_sha256=digest, reviewer='independent-opus-direct-image', + rows=[dict(row_index=0, category=True, state=True, position=True)]) + for change in ('wrong_sha', 'missing_row', 'extra_row', 'rejected_state'): + bad = copy.deepcopy(verdict) + if change == 'wrong_sha': bad['manifest_sha256'] = '0'*64 + if change == 'missing_row': bad['rows'] = [] + if change == 'extra_row': bad['rows'] *= 2 + if change == 'rejected_state': bad['rows'][0]['state'] = False + verdict_path.write_text(json.dumps(bad)) + with self.subTest(change=change), self.assertRaises(ValueError): + check.reviewed_pixel_categories(manifest, digest, verdict_path) + verdict_path.write_text(json.dumps(verdict)) + self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), {'staff'}) + manifest['mechanical_pass'] = False + self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set()) + + +class Repair36LabelTests(unittest.TestCase): + inputs = check.ROOT / 'tests/fixtures/p23-03/recorded-badge-negative.json.gz' + + def _recorded_payloads(self): + bundle = json.loads(gzip.decompress(self.inputs.read_bytes())) + expected = { + 'request': (655285, 'f9cd2d5ff9dd63d9cf7ae4a295ccef8eea16f75ac784696dc40906703731e1d8'), + 'state': (1867011, '724d8fe362ba3e05eddd83c4b7c54da101899df6cd91ae88250feea3d4feff82'), + 'manifest': (75726, 'a98782ec1263aba400d542862dd084d2f1dd4cf7a0851cf7f0679867e545a338'), + } + payloads = {} + for name, (length, digest) in expected.items(): + payload = bundle['files'][name]['raw_utf8'].encode('utf-8') + check.require(len(payload) == length and hashlib.sha256(payload).hexdigest() == digest, + 'recorded fixture payload mismatch: ' + name) + payloads[name] = payload + return payloads + + def recorded(self): + payloads = self._recorded_payloads() + request = json.loads(payloads['request']) + typed_state = json.loads(payloads['state']) + check.require(typed_state.get('type') == 'Dictionary', 'whole dictionary required') + return request, check.decode(typed_state), check.decode(request['report']) + + def resolved(self, report): + result = copy.deepcopy(report) + for collection, index, fields in check.observation_label_layout(report): + result[collection][index].update(fields) + return result + + def test_recorded_negative_and_positive_with_raw_semantics(self): + request, state, old = self.recorded() + rectangles = check.drawing_rectangle_rows(old) + self.assertTrue(check.intersects(rectangles[5][2], rectangles[26][2])) + with self.assertRaises(ValueError): + check.validate_observation_labels(old) + report = self.resolved(old) + check.validate_observation_labels(report) + check.compare_queues(state, report) + check.compare_badges(state, report) + check.compare_actor_geometry(state, report) + queue = next(b for b in report['landing_badges'] if b['visible_count'] == 0) + manifest = json.loads(self._recorded_payloads()['manifest']) + original = manifest['rows'][5] + self.assertEqual(queue['anchor'], original['local_rect'][:2]) + self.assertEqual(original['screenshot_rect'][:2], [642,469]) + self.assertEqual(queue['members'], next(b['members'] for b in old['landing_badges'] if b['visible_count'] == 0)) + + def test_recorded_forgery_controls(self): + _, state, old = self.recorded() + good = self.resolved(old) + for attack in ('at', 'displaced_by', 'count', 'hidden', 'anchor'): + report = copy.deepcopy(good) + row = report['actor_aggregations'][0] + if attack == 'at': row['at'][0] += 1 + elif attack == 'displaced_by': row['displaced_by'] = [] + elif attack == 'count': row['text'] = '+' + elif attack == 'hidden': report['actor_aggregations'] = [] + else: row['anchor'][0] += 1 + with self.subTest(attack=attack), self.assertRaises(ValueError): + check.validate_observation_labels(report) + check.compare_queues(state, report) + check.compare_actor_geometry(state, report) + + def test_generic_order_and_edge_contact(self): + for kinds in [('business', 'aggregation'), ('supply', 'landing'), + ('business', 'supply', 'landing', 'aggregation')]: + report = dict(cabins=[], badges=[], landing_badges=[], actor_aggregations=[]) + for kind in kinds: + key = 'badges' if kind in ('business', 'supply') else 'landing_badges' if kind == 'landing' else 'actor_aggregations' + report[key].append(dict(kind=kind, at=[10,20], text='abc')) + result = self.resolved(report) + check.validate_observation_labels(result) + positions = [fields['at'] for _, _, fields in check.observation_label_layout(result)] + self.assertEqual(positions, [[10+36*i,20] for i in range(len(kinds))]) + report = dict(cabins=[dict(rect=[0,50,20,20], badge_at=[10,20], badge='abc')], + badges=[], landing_badges=[dict(at=[10,20], text='x')], actor_aggregations=[]) + self.assertEqual(self.resolved(report)['landing_badges'][0]['at'], [46,20]) + report['landing_badges'][0]['at'] = [46,20] + self.assertEqual(self.resolved(report)['landing_badges'][0]['displaced_by'], []) + + def test_future_review_requires_indexed_judgments(self): + manifest = dict(rows=[dict(row_index=7, category='queue', eligibility='visible', mechanical_pass=True)], mechanical_pass=True) + verdict = dict(manifest_sha256='digest', reviewer='independent-opus-direct-image', + state_bits='1', position_bits='1', category_bits='1') + with tempfile.TemporaryDirectory() as folder: + path = Path(folder)/'verdict.json' + path.write_text(json.dumps(verdict)) + with self.assertRaisesRegex(ValueError, 'indexed judgments'): + check.reviewed_pixel_categories(manifest, 'digest', path, require_judgments=True) + verdict['judgments'] = [dict(row_index=7, category=True, state=True, position=True)] + path.write_text(json.dumps(verdict)) + self.assertEqual(check.reviewed_pixel_categories(manifest, 'digest', path, require_judgments=True), {'queue'}) + + +if __name__ == '__main__': + unittest.main()