From 03100beabeb55caac03f53db3d1f399b424df517 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 07:59:43 +0900 Subject: [PATCH 01/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 11 + .../execution/p23-03/API_AND_STATE_MAP.md | 41 ++ .../execution/p23-03/WORKER_STATUS.md | 94 ++++ scenes/main/world_preview.gd | 347 +++++++++---- scripts/ui/room_presentation.gd | 49 +- tests/fixtures/p23-03/diagnostic_stream.gd | 56 ++ .../fixtures/p23-03/diagnostic_stream.gd.uid | 1 + tests/fixtures/p23-03/layout_comparison.gd | 104 ++++ tests/ui/test_p23_life_observation.gd | 77 +++ tests/ui/test_p23_life_observation.gd.uid | 1 + tools/check_p23_life_observation_evidence.py | 491 ++++++++++++++++++ tools/run_p23_life_observation.py | 25 + tools/test_p23_life_observation_evidence.py | 190 +++++++ 13 files changed, 1386 insertions(+), 101 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/API_AND_STATE_MAP.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md create mode 100644 tests/fixtures/p23-03/diagnostic_stream.gd create mode 100644 tests/fixtures/p23-03/diagnostic_stream.gd.uid create mode 100644 tests/fixtures/p23-03/layout_comparison.gd create mode 100644 tests/ui/test_p23_life_observation.gd create mode 100644 tests/ui/test_p23_life_observation.gd.uid create mode 100644 tools/check_p23_life_observation_evidence.py create mode 100644 tools/run_p23_life_observation.py create mode 100644 tools/test_p23_life_observation_evidence.py diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 6539ba6..079c8a4 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 @@ -3702,6 +3702,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": "b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0", + "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/WORKER_STATUS.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md new file mode 100644 index 0000000..d9c13e9 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/WORKER_STATUS.md @@ -0,0 +1,94 @@ +# 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/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index b6a87a2..15b2366 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -78,6 +78,7 @@ var _queue_positions: Dictionary = {} var _queue_hidden: Dictionary = {} var _landing_badges: Array[Dictionary] = [] var _transport_report: Dictionary = {} +var _life_observation: Dictionary = {} var _dragging := false var _drag_button := MOUSE_BUTTON_NONE @@ -511,6 +512,7 @@ func _draw() -> void: func _draw_contents() -> void: + _life_observation = {} if _inactive_layer_art != null: _inactive_layer_art.visible = false _draw_background() @@ -521,15 +523,25 @@ func _draw_contents() -> void: var map_rect := get_map_rect() if map_rect.size == Vector2.ZERO: return + _life_observation = _build_life_observation() + _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_congestion_overlay(bounds) + _draw_life_badges() _draw_construction_labels() _draw_facility_labels() _draw_actors() @@ -540,6 +552,102 @@ 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 + var bridge: Variant = _service(&"supply_logistics_bridge") + 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 + 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(): + traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)}) + model[&"traffic"] = traffic + return model + + +func _draw_life_badges() -> void: + for badge: Dictionary in _life_observation.get(&"badges", []): + _draw_readable_badge(badge[&"at"], badge[&"text"]) + + func _draw_background() -> void: var night := false var clock := get_node_or_null("/root/GameClock") @@ -650,40 +758,23 @@ 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"): - 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) +func _draw_congestion_overlay(_bounds: Dictionary) -> void: + for row: Dictionary in _life_observation.get(&"traffic", []): + var rect: Rect2 = row[&"rect"] + 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") + var center := rect.get_center() + 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 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0)) + if blocked: + draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0) + draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0) ## Visual-only level comes from the existing live FacilityInstance, not the @@ -794,10 +885,6 @@ 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: @@ -810,13 +897,21 @@ func _draw_readable_badge(at: Vector2, text: String) -> void: 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 +939,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") @@ -908,10 +1013,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} func _add_queue_member(groups: Dictionary, key: String, cell: Vector3i) -> void: @@ -1039,14 +1144,21 @@ func _draw_facility_labels() -> void: 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 + for row: Dictionary in _life_observation.get(&"cabins", []): + PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"]) + for actor: Dictionary in row[&"passengers"]: + _draw_observed_actor(actor) + PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"]) + _draw_readable_badge(row[&"badge_at"], row[&"badge"]) + for badge: Dictionary in _life_observation.get(&"landing_badges", []): + _draw_readable_badge(badge[&"at"], badge[&"text"]) + + +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 +1174,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,7 +1186,7 @@ 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): @@ -1090,22 +1202,19 @@ func _draw_elevators() -> void: 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 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, + &"passengers": passengers_drawn, + &"badge_at": Vector2(cabin.position.x, cabin.position.y-14), + &"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 +1224,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] @@ -1131,6 +1241,19 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float) -> floa func _draw_actors() -> void: + for row: Dictionary in _life_observation.get(&"actor_drawings", []): + _draw_observed_actor(row) + + +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 +1267,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 +1289,29 @@ 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 _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 +1326,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 +1353,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 +1373,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 +1409,19 @@ 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) -> void: + var at: Vector2 = row[&"at"] + var p: float = row[&"scale"] + draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2)) + People.draw(self, 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 +1448,10 @@ 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} func _draw_excavation_jobs() -> void: @@ -1421,14 +1564,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 +1722,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 +1737,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..20dd175 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,48 @@ 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 + return value + + +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..fb2706a --- /dev/null +++ b/tests/fixtures/p23-03/diagnostic_stream.gd @@ -0,0 +1,56 @@ +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 + +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) + if state_hash.is_empty() or report_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, + "input_method": "diagnostic", "acceptance": "pending"})) + manifest.flush() + return var_to_bytes(state) == var_to_bytes(runtime.create_snapshot()) + +func close() -> void: + if manifest != null: + manifest.close() + manifest = null + +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..0f67a0e --- /dev/null +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -0,0 +1,104 @@ +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) + 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. + ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute, + &"guests": runtime.guest_service.create_snapshot(), &"path": runtime.path_service.create_snapshot(), + &"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok + for guest: GuestRuntime in runtime.guest_service.get_guests(): + if guest.current_cell != GOAL or finished.has(guest.guest_id): continue + finished[guest.guest_id] = offset + var leaving: Dictionary = runtime.guest_service.begin_leaving(guest.guest_id, clock.absolute_minute, &"COMPARISON_DESTINATION_REACHED") + var despawn: Dictionary = runtime.guest_service.despawn_guest(guest.guest_id, clock.absolute_minute) + ok = bool(leaving.get(&"ok", false)) and bool(despawn.get(&"ok", false)) and ok + if offset % 30 == 0: + view.queue_redraw() + await tree.process_frame + await tree.process_frame + ok = stream.capture("frame-%03d" % offset, runtime, view) 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/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd new file mode 100644 index 0000000..5b023b6 --- /dev/null +++ b/tests/ui/test_p23_life_observation.gd @@ -0,0 +1,77 @@ +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 + +func _initialize() -> void: + call_deferred("run") + +func expect(value: bool, message: String) -> void: + checks += 1 + if not value: + failures += 1 + push_error("P23 life observation: " + message) + +func run() -> void: + var clock: Variant = root.get_node("GameClock") + clock.set_paused(true) + # 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) + 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") + # 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..6c1e697 --- /dev/null +++ b/tools/check_p23_life_observation_evidence.py @@ -0,0 +1,491 @@ +#!/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 copy +import hashlib +import importlib.util +import json +import math +from pathlib import Path +import re +import sys + +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', +) +BLOCKERS = { + CONDITIONS[0]: 'Service/rescue occupancy, reason fields and room rectangles checked; rest route selection and badge pixels unfinished.', + CONDITIONS[1]: 'Queue lane allocation, overflow and positions checked; screenshot pixel provenance unfinished.', + CONDITIONS[2]: 'Shaft/trip and cabin geometry checked; prior-frame interpolation history and passenger geometry unfinished.', + CONDITIONS[3]: 'Typed staff/task projection implemented; motion/rest/rescue/cleaning drawing recomputation unfinished.', + CONDITIONS[4]: 'Carried inventory ID and logistics report checked; supply badge pixels unfinished.', + CONDITIONS[5]: 'Public-command A/B fixture and typed arrival metric added; native execution, definition pins and full command/path provenance checks pending.', + CONDITIONS[6]: 'P23 passive request/ACK collector and independent CUA/formal-resume gates unfinished; Root evidence missing.', +} + + +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 sha(path): + return hashlib.sha256(Path(path).read_bytes()).hexdigest() + + +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'] + 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']), + waiting_count=sum(t['shaft_id'] == shaft['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 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) + rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'}, + rect=[x + 2, y + 2, w - 4, h - 4])) + close(report['traffic'], rows, 'traffic and cell rect') + + +def compare_queues(snapshot, 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']) + lift_cells = {tuple(cell) for shaft in elevators['shafts'] for cell in shaft['stops']} + 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 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 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_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 + expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'], + doors_open=shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING'), + 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) + + +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 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']) + report = read_object(directory, row['report']) + fields = compare_actor_rows(state, report, root) + require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch') + compare_room_services(state, report, root) + compare_queues(state, report) + compare_traffic(state, report) + compare_goods(state, report, root) + compare_cabin_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(): + return [dict(condition=name, status='blocked', reason=BLOCKERS[name]) for name in CONDITIONS] + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--diagnostics', type=Path) + parser.add_argument('--layout-a', type=Path) + parser.add_argument('--layout-b', type=Path) + args = parser.parse_args(argv) + # 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..25d2cbd --- /dev/null +++ b/tools/run_p23_life_observation.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +"""Read-only P23 readiness report. Collection is blocked in this candidate. + +No runner, engine, profile, subprocess, browser or OS input is started. Root must +not mistake this preparation interface for a submitted native/OS collector. +""" +import json +import sys + +sys.dont_write_bytecode = True +from check_p23_life_observation_evidence import condition_matrix + + +def main(): + print(json.dumps({'task_id': 'P23-03', 'status': 'blocked', 'engine_runs': 0, + 'condition_matrix': condition_matrix(), + 'required_infrastructure': ['run_all_tests.py', 'strict_test_markers.py', + 'regression/owned_outer.py', + 'regression/ordinary_proof_gate.py'], + 'root_evidence': 'missing'}, ensure_ascii=False)) + return 2 + + +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..db46ca6 --- /dev/null +++ b/tools/test_p23_life_observation_evidence.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Local semantic-checker tests; synthetic examples are never game evidence.""" +import copy +import unittest +import sys + +sys.dont_write_bytecode = True +import check_p23_life_observation_evidence as check + + +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 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], + 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=[])) + + 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): + self.runtime['elevators']['trips'] = [dict(actor_id='guest:g'+str(i), enqueued_at=i, + state='QUEUED', **{'from': [0, 0, 0]}) for i in range(3)] + self.report.update(queue_positions={'guest/g0': [84, 82], 'guest/g1': [100, 82]}, + queue_hidden={'guest/g2': True}, landing_badges=[dict(at=[76, 54], + text='待ち3人(他1人)', cell=[0, 0, 0], + members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=2, overflow=1)]) + 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(shaft_id='lift', config=dict(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=[])]) + 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 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) + + +if __name__ == '__main__': + unittest.main() From 4c8a18aa7554c12552f62e0c4ec6e6ecc8dde8e5 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:36:36 +0900 Subject: [PATCH 02/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../execution/p23-03/WORKER_STATUS.md | 202 +++++ tests/fixtures/p23-03/diagnostic_stream.gd | 90 +- tests/fixtures/p23-03/layout_comparison.gd | 14 +- tests/fixtures/p23-03/observation.gd | 161 ++++ tests/fixtures/p23-03/passive_observation.gd | 254 ++++++ tests/fixtures/p23-03/passive_os_recorder.gd | 8 + tests/ui/test_p23_life_observation.gd | 8 + tools/check_p23_life_observation_evidence.py | 839 +++++++++++++++++- tools/run_p23_life_observation.py | 357 +++++++- tools/test_p23_life_observation_evidence.py | 307 +++++++ 11 files changed, 2207 insertions(+), 35 deletions(-) create mode 100644 tests/fixtures/p23-03/observation.gd create mode 100644 tests/fixtures/p23-03/passive_observation.gd create mode 100644 tests/fixtures/p23-03/passive_os_recorder.gd 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 079c8a4..82e2931 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 @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0", + "source_sha256_at_schema_audit": "cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1", "authority": { "path": "docs/spec/acceptance/P23-03.json", "scope": "component and read-only observation only; native unexecuted; whole unit blocked" 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 index d9c13e9..0303b76 100644 --- 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 @@ -1,3 +1,205 @@ +# 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`. diff --git a/tests/fixtures/p23-03/diagnostic_stream.gd b/tests/fixtures/p23-03/diagnostic_stream.gd index fb2706a..fd4992e 100644 --- a/tests/fixtures/p23-03/diagnostic_stream.gd +++ b/tests/fixtures/p23-03/diagnostic_stream.gd @@ -3,6 +3,40 @@ extends RefCounted 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) + 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 after_draw() -> void: + if not is_instance_valid(view): return + var report := view.get_life_observation_report() + if report.is_empty(): return + var sample := {} + for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]: + sample[key] = report[key] + 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": Engine.get_process_frames(), "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 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 open(path: String) -> bool: directory = path @@ -33,19 +67,71 @@ func capture(label: String, runtime: Variant, view: WorldPreview) -> bool: var report := view.get_life_observation_report() var state_hash := object(state) var report_hash := object(report) - if state_hash.is_empty() or report_hash.is_empty(): + 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(runtime.create_snapshot()) + 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) diff --git a/tests/fixtures/p23-03/layout_comparison.gd b/tests/fixtures/p23-03/layout_comparison.gd index 0f67a0e..4158841 100644 --- a/tests/fixtures/p23-03/layout_comparison.gd +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -62,6 +62,7 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool: 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 @@ -77,20 +78,19 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool: 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": runtime.guest_service.create_snapshot(), &"path": runtime.path_service.create_snapshot(), + &"guests": guest_records, &"path": runtime.path_service.create_snapshot(), &"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok - for guest: GuestRuntime in runtime.guest_service.get_guests(): - if guest.current_cell != GOAL or finished.has(guest.guest_id): continue - finished[guest.guest_id] = offset - var leaving: Dictionary = runtime.guest_service.begin_leaving(guest.guest_id, clock.absolute_minute, &"COMPARISON_DESTINATION_REACHED") - var despawn: Dictionary = runtime.guest_service.despawn_guest(guest.guest_id, clock.absolute_minute) - ok = bool(leaving.get(&"ok", false)) and bool(despawn.get(&"ok", false)) and ok + for guest: Dictionary in guest_records[&"guests"]: + if guest[&"current_cell"] != GOAL or finished.has(guest[&"guest_id"]): continue + finished[guest[&"guest_id"]] = offset if offset % 30 == 0: view.queue_redraw() await tree.process_frame await tree.process_frame ok = 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() diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd new file mode 100644 index 0000000..b077ab1 --- /dev/null +++ b/tests/fixtures/p23-03/observation.gd @@ -0,0 +1,161 @@ +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") +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) + ": " + String(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(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 definition-derived diagnostic construction budget. + # 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 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 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} + 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 [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], + [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]: + 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"}) + command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"], + &"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40}) + 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(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report") + await button(main.back_button) + 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/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd new file mode 100644 index 0000000..ea1b577 --- /dev/null +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -0,0 +1,254 @@ +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(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(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 + 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(&"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"])) + 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(width, 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_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/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index 5b023b6..fb9f077 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -15,6 +15,14 @@ func expect(value: bool, message: String) -> void: 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) # Isolated real typed components test state labels, without claiming gameplay. diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 6c1e697..048ead2 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -10,9 +10,12 @@ import hashlib import importlib.util import json import math +import os from pathlib import Path import re import sys +import struct +import zlib sys.dont_write_bytecode = True ROOT = Path(__file__).resolve().parent.parent @@ -24,15 +27,13 @@ CONDITIONS = ( 'supplies_and_carried_goods', 'same_demand_route_improvement', 'normal_os_and_distinct_pid_resume', ) -BLOCKERS = { - CONDITIONS[0]: 'Service/rescue occupancy, reason fields and room rectangles checked; rest route selection and badge pixels unfinished.', - CONDITIONS[1]: 'Queue lane allocation, overflow and positions checked; screenshot pixel provenance unfinished.', - CONDITIONS[2]: 'Shaft/trip and cabin geometry checked; prior-frame interpolation history and passenger geometry unfinished.', - CONDITIONS[3]: 'Typed staff/task projection implemented; motion/rest/rescue/cleaning drawing recomputation unfinished.', - CONDITIONS[4]: 'Carried inventory ID and logistics report checked; supply badge pixels unfinished.', - CONDITIONS[5]: 'Public-command A/B fixture and typed arrival metric added; native execution, definition pins and full command/path provenance checks pending.', - CONDITIONS[6]: 'P23 passive request/ACK collector and independent CUA/formal-resume gates unfinished; Root evidence missing.', -} +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): @@ -74,6 +75,199 @@ 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): + """CUA receipt and converter pin are independent of conversion assertions.""" + raw, png = Path(rec['raw_path']), 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', str(raw), '--out', str(png)], + '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 compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, local_to_screen, image, root=ROOT): + """All draw rows, full affine composition, and exact existing style colors. + Font advance is recorded by the pinned engine/font, not approximated using + a new text width threshold. No image similarity threshold is introduced. + """ + for point in ([0, 0], [1, 0], [0, 1]): + close(affine_point(viewport_to_screen, affine_point(local_to_viewport, point)), + affine_point(local_to_screen, point), 'viewport/screen composition') + rows = audit['rows'] + expected = [] + tower, people, world = 'scripts/ui/pixel_tower_art.gd', 'scripts/ui/pixel_people.gd', '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, at, text): + expected.append((category, identifier, at + [None, 14], world, 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['at'], b['text']) + 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['badges']): + badge('goods' if b['kind'] == 'supply' else 'business', str(i), b['at'], b['text']) + require(len(rows) == len(expected), 'rectangle coverage') + width, height, pixels = image + checked = [] + for row, (category, identifier, rect, source, text) in zip(rows, expected): + require((row['category'], row['id'], row['style_source']) == (category, identifier, source), 'rectangle row identity') + if text is not None: + require(row['text'] == text and type(row['font_width']) in (int, float) and row['font_width'] > 0, 'badge font/text') + rect[2] = row['font_width'] + 6 + close(row['rect'], rect, 'local drawing rectangle') + viewport = affine_rect(local_to_viewport, rect) + screen = affine_rect(viewport_to_screen, viewport) + close(row['viewport_rect'], viewport, 'viewport rectangle') + close(row['screen_rect'], screen, 'screen 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, 'drawing style palette') + x, y, w, h = screen + require(w > 0 and h > 0 and x >= 0 and y >= 0 and x+w <= width and y+h <= height, + 'drawing rectangle outside screenshot') + # Badge ink is distinct from its translucent backing. Actor ink/skin + # and facility colors are taken verbatim from their pinned draw source. + palette = {'fff2ce'} if text is not None else set(colors) + palette -= {'303c40', 'c6d3b6', 'd5dec4'} # shadow / sky decorations + rgb = {tuple(bytes.fromhex(c)) for c in palette} + found = any(tuple(pixels[py][px][:3]) in rgb + for py in range(math.ceil(y), math.floor(y+h)) + for px in range(math.ceil(x), math.floor(x+w))) + require(found, 'missing drawing pixels: ' + category + '/' + str(identifier)) + checked.append(category + '/' + str(identifier)) + return checked + + def enum_names(root, relative, name): source = (root / relative).read_text() match = re.search(r'\benum\s+' + re.escape(name) + r'\s*\{([^}]+)\}', source) @@ -322,6 +516,117 @@ def comparison_records(directory): 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') + transform = Transform(runtime, report) + drawings = {r['key']: r for r in report['actor_drawings']} + for sid, fid in resting.items(): + member = next(m for m in members if m['staff_id'] == sid) + if member['current_cell'][2] != report['layer'] or sid in report['carried_patients']: + continue + room = report['rooms'].get(fid, {}) + cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), []) + if not cells: + continue # renderer uses its ordinary tracked fallback + require(sid in room.get('people', []), 'rest room occupant missing') + slot = room['people'].index(sid) + anchors = room_slots(transform.room(cells), transform.dim[0] / 16) + key = 'staff/' + sid + if key in report['boarded'] or key in report['queue_hidden']: + require(key not in drawings, 'hidden resting drawing') + elif slot >= len(anchors): + require(key not in drawings, 'rest slot overflow drawing') + else: + close(drawings[key]['center'], [anchors[slot][0], anchors[slot][1] - transform.dim[1] * .5], + 'resting staff position') + return selections + + def compare_goods(snapshot, report, root=ROOT): runtime = snapshot['systems']['runtime'] states = enum_names(root, 'scripts/inventory/logistics_request.gd', 'State') @@ -356,6 +661,42 @@ def compare_goods(snapshot, report, root=ROOT): close(report['goods'], dict(requests=rows, deliveries=active + [r[2] for r in recent[:10]]), 'goods') +def compare_badges(state, 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) @@ -394,6 +735,173 @@ def compare_cabin_geometry(snapshot, report): close(row[key], value, 'cabin/' + key) +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 compare_actor_geometry(state, report): + runtime = state['systems']['runtime'] + 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']} + counts, expected = {}, [] + def track_position(key, cell): + track = report['actor_tracks'][key] + close(track['cell'], cell, 'actor authoritative 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 + 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 = 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 = track_position(key, cell) + special = True + if 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 + 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)) + require([r['key'] for r in report['actor_drawings']] == [r[0] for r in expected], 'actor drawing coverage/order') + pairs = list(zip(report['actor_drawings'], expected)) + 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'][:columns*2] + require(len(cabin['passengers']) == len(passengers), 'passenger coverage') + for i, (actor_id, row) in enumerate(zip(passengers, cabin['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)*9)*p+3*p-height*.5] + carried = bool(staff.get(key.removeprefix('staff/'), {}).get('carried_inventory_id')) if is_staff else False + pairs.append((row, (key, center, is_staff, False, carried, True))) + for row, (key, center, is_staff, receiving, carrying, riding) in pairs: + close(row['center'], center, 'actor center/interpolation') + scale = .5 if riding else .75 + scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale + close(row['scale'], scale, 'actor 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] + close(row['at'], rounded, 'actor drawing position') + require((row['key'], row['staff'], row['receiving'], row['carrying'], row['riding']) == + (key, is_staff, receiving, carrying, riding), 'actor drawing attributes') + return ['actor_geometry', 'passenger_geometry'] + + def compare_layouts(a, b): require(a['declaration'] == b['declaration'], 'identical predeclared inputs') declaration = a['declaration'] @@ -452,30 +960,339 @@ def inspect_stream(directory, root=ROOT): 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') 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(): - return [dict(condition=name, status='blocked', reason=BLOCKERS[name]) for name in CONDITIONS] +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' + + +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 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(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] + close(read_object(stream, capture['report']), report, 'request draw report binding') + 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': + checked = compare_rectangles(report, decode(request['rectangles']), request['local_to_viewport'], + request['viewport_to_screen'], request['local_to_screen'], image, root) + pixel_categories.update(c.split('/')[0] for c in checked) + 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): + """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()) + 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') + 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) + inspect_native_phases(directory, index, root) + 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('--layout-a', type=Path) parser.add_argument('--layout-b', type=Path) args = parser.parse_args(argv) + if args.verify_only: + build = os.environ.get('BUILD_OUTPUT_DIR') + if not build: + print('BUILD_OUTPUT_DIR is required', file=sys.stderr) + return 2 + deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence') + 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') diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py index 25d2cbd..fe5fc72 100644 --- a/tools/run_p23_life_observation.py +++ b/tools/run_p23_life_observation.py @@ -1,25 +1,354 @@ #!/usr/bin/env python3 -"""Read-only P23 readiness report. Collection is blocked in this candidate. - -No runner, engine, profile, subprocess, browser or OS input is started. Root must -not mistake this preparation interface for a submitted native/OS collector. -""" +"""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 -from check_p23_life_observation_evidence import condition_matrix +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 = 900 +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. -def main(): - print(json.dumps({'task_id': 'P23-03', 'status': 'blocked', 'engine_runs': 0, - 'condition_matrix': condition_matrix(), - 'required_infrastructure': ['run_all_tests.py', 'strict_test_markers.py', - 'regression/owned_outer.py', - 'regression/ordinary_proof_gate.py'], - 'root_evidence': 'missing'}, ensure_ascii=False)) - return 2 + 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 + 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 final submitted commit 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) + 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, 'tests/ui/test_p23_life_observation.gd') + cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)}) + 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-v1', 'commit':commit, + '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('--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 index db46ca6..4c149b4 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -3,11 +3,318 @@ import copy import unittest import sys +import contextlib +import io +import json +import hashlib +import struct +import zlib +import tempfile +from pathlib import Path +from unittest import mock sys.dont_write_bytecode = True import check_p23_life_observation_evidence as check +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(dir=check.ROOT/'tests/fixtures/p23-03') 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_rectangle_pixel_and_transform_negatives(self): + 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()))) + report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[], + actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)]) + row = 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) + audit = dict(rows=[row]) + matrix = [1, 0, 0, 1, 0, 0] + image = (24, 24, [[(113, 157, 148)]*24 for _ in range(24)]) + self.assertEqual(check.compare_rectangles(report, audit, matrix, matrix, matrix, image), ['staff/staff/s']) + for change in ('rectangle', 'pixels', 'transform', 'coverage', 'palette'): + with self.subTest(change=change): + a, m, img = copy.deepcopy(audit), matrix[:], image + if change == 'rectangle': a['rows'][0]['rect'][0] += 1 + if change == 'pixels': img = (24, 24, [[(0, 0, 0)]*24 for _ in range(24)]) + if change == 'transform': m[4] = 10 + if change == 'coverage': a['rows'].clear() + if change == 'palette': a['rows'][0]['style_colors'] = ['000000'] + with self.assertRaises(ValueError): check.compare_rectangles(report, a, matrix, m, matrix, img) + + 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', 'display'): + 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 == '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], From f6176d3f19d0cbfe0342042da6bac4e6f257ae5b Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:39:55 +0900 Subject: [PATCH 03/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures/p23-03/layout_comparison.gd.uid | 1 + tests/fixtures/p23-03/observation.gd.uid | 1 + tests/fixtures/p23-03/passive_observation.gd.uid | 1 + tests/fixtures/p23-03/passive_os_recorder.gd.uid | 1 + 4 files changed, 4 insertions(+) create mode 100644 tests/fixtures/p23-03/layout_comparison.gd.uid create mode 100644 tests/fixtures/p23-03/observation.gd.uid create mode 100644 tests/fixtures/p23-03/passive_observation.gd.uid create mode 100644 tests/fixtures/p23-03/passive_os_recorder.gd.uid 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.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.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.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 From d9e4e9bd2f86c6576d4ac6ddefab0ceb7005dff9 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:42:03 +0900 Subject: [PATCH 04/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures/p23-03/observation.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd index b077ab1..25383a9 100644 --- a/tests/fixtures/p23-03/observation.gd +++ b/tests/fixtures/p23-03/observation.gd @@ -28,7 +28,7 @@ func setup() -> void: func command(id: StringName, payload: Dictionary) -> bool: var result: CommandResult = runtime.submit_command(id, payload) - expect(result.is_success(), String(id) + ": " + String(result.code)) + expect(result.is_success(), String(id) + ": " + 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() From 59a1385536b15baf2545f367efdaff0bd5cffd51 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:44:55 +0900 Subject: [PATCH 05/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/check_p23_life_observation_evidence.py | 2 +- tools/run_p23_life_observation.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 048ead2..9f80b39 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -1161,7 +1161,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): 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')] + 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 diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py index fe5fc72..6592539 100644 --- a/tools/run_p23_life_observation.py +++ b/tools/run_p23_life_observation.py @@ -129,7 +129,7 @@ def collect_os(r, root, out, stage, engine, commit, refs, protected, outer): 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 + 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 @@ -306,7 +306,7 @@ def collect(args): for layout in ('A', 'B'): directory = out/('layout-'+layout) os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory)) - phase('layout-'+layout, 'tests/ui/test_p23_life_observation.gd') + phase('layout-'+layout.lower(), 'tests/ui/test_p23_life_observation.gd') cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)}) if args.input_mode == 'native_os': os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer) From baa4f2d5632127038c18d1843fc55e8aed587382 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:47:27 +0900 Subject: [PATCH 06/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures/p23-03/layout_comparison.gd | 2 +- tools/test_p23_life_observation_evidence.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/p23-03/layout_comparison.gd b/tests/fixtures/p23-03/layout_comparison.gd index 4158841..8c42e50 100644 --- a/tests/fixtures/p23-03/layout_comparison.gd +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -83,7 +83,7 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool: &"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 or finished.has(guest[&"guest_id"]): continue + 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: view.queue_redraw() diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 4c149b4..d7eecb1 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Local semantic-checker tests; synthetic examples are never game evidence.""" import copy +import ast import unittest import sys import contextlib @@ -17,6 +18,26 @@ sys.dont_write_bytecode = True import check_p23_life_observation_evidence as check +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') From 55c29f119234dd6f993c604c4edc6f377fe3951c Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 09:52:50 +0900 Subject: [PATCH 07/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures/p23-03/diagnostic_stream.gd | 33 +++++++++++++++++--- tests/fixtures/p23-03/layout_comparison.gd | 6 ++-- tests/fixtures/p23-03/observation.gd | 3 ++ tests/fixtures/p23-03/passive_observation.gd | 3 ++ tools/test_p23_life_observation_evidence.py | 6 +++- 5 files changed, 41 insertions(+), 10 deletions(-) diff --git a/tests/fixtures/p23-03/diagnostic_stream.gd b/tests/fixtures/p23-03/diagnostic_stream.gd index fd4992e..7f51e24 100644 --- a/tests/fixtures/p23-03/diagnostic_stream.gd +++ b/tests/fixtures/p23-03/diagnostic_stream.gd @@ -12,25 +12,35 @@ class MotionObserver extends Node: 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 after_draw() -> void: - if not is_instance_valid(view): return - var report := view.get_life_observation_report() - if report.is_empty(): return + 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": Engine.get_process_frames(), "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample}) + 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: @@ -38,6 +48,19 @@ func watch(view: WorldPreview) -> void: 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): diff --git a/tests/fixtures/p23-03/layout_comparison.gd b/tests/fixtures/p23-03/layout_comparison.gd index 8c42e50..4a38945 100644 --- a/tests/fixtures/p23-03/layout_comparison.gd +++ b/tests/fixtures/p23-03/layout_comparison.gd @@ -86,10 +86,8 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool: 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: - view.queue_redraw() - await tree.process_frame - await tree.process_frame - ok = stream.capture("frame-%03d" % offset, runtime, view) and ok + 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() diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd index 25383a9..8b9a2ed 100644 --- a/tests/fixtures/p23-03/observation.gd +++ b/tests/fixtures/p23-03/observation.gd @@ -39,6 +39,7 @@ func advance(minutes: int) -> void: 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: @@ -118,8 +119,10 @@ func prepare() -> void: 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: diff --git a/tests/fixtures/p23-03/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd index ea1b577..1d9203e 100644 --- a/tests/fixtures/p23-03/passive_observation.gd +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -44,10 +44,12 @@ func motion_observations() -> void: 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: @@ -93,6 +95,7 @@ func request(action: Dictionary) -> void: 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()) diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index d7eecb1..536fa73 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -269,11 +269,15 @@ class InfrastructureTests(unittest.TestCase): 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', 'display'): + 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) From 0b7727a66c76d1ce2a345571718feb3117dd8802 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 10:03:36 +0900 Subject: [PATCH 08/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p23-03/OFFLINE_REPAIR_55c29f1.md | 72 +++++++++++++++++++ tests/fixtures/p23-03/queued_elevator.json | 27 +++++++ tools/check_p23_life_observation_evidence.py | 22 +++++- tools/test_p23_life_observation_evidence.py | 64 +++++++++++++++-- 4 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_55c29f1.md create mode 100644 tests/fixtures/p23-03/queued_elevator.json 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/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/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 9f80b39..c582983 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -314,6 +314,25 @@ def compare_actor_rows(snapshot, report, root=ROOT): 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) @@ -325,7 +344,8 @@ def elevator_report(snapshot): 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']), - waiting_count=sum(t['shaft_id'] == shaft['shaft_id'] and t['state'] == 'QUEUED' + # 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. diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 536fa73..609eab9 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -18,6 +18,54 @@ sys.dont_write_bytecode = True import check_p23_life_observation_evidence as check +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() @@ -431,12 +479,14 @@ class SpatialTests(unittest.TestCase): check.compare_traffic(self.snapshot, self.report) def test_missing_queue_overflow_rejected(self): - self.runtime['elevators']['trips'] = [dict(actor_id='guest:g'+str(i), enqueued_at=i, - state='QUEUED', **{'from': [0, 0, 0]}) for i in range(3)] - self.report.update(queue_positions={'guest/g0': [84, 82], 'guest/g1': [100, 82]}, - queue_hidden={'guest/g2': True}, landing_badges=[dict(at=[76, 54], - text='待ち3人(他1人)', cell=[0, 0, 0], - members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=2, overflow=1)]) + 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(queue_positions={'guest/g0': [100, 82]}, + queue_hidden={'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54], + text='待ち3人(他2人)', cell=[0, 0, 0], + members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=1, overflow=2)]) check.compare_queues(self.snapshot, self.report) self.report['landing_badges'][0]['overflow'] = 0 with self.assertRaises(ValueError): @@ -470,7 +520,7 @@ class SpatialTests(unittest.TestCase): check.compare_room_services(self.snapshot, changed) def test_cabin_geometry_and_phase(self): - shaft = dict(shaft_id='lift', config=dict(stops=[[0, 0, 0]], width=2, capacity=4), + 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)}, From 21b5d3a31ad35aeb5a17a033af83985b454b4d8f Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 10:32:29 +0900 Subject: [PATCH 09/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OFFLINE_MIRROR_106_DIAGNOSTICS.json | 1 + .../p23-03/OFFLINE_REPAIR_0b7727a_BLOCKED.md | 253 +++ ...FLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json | 1953 +++++++++++++++++ .../writer-request-0002/manifest.json | 922 ++++++++ .../writer-request-0002/manifest.sha256 | 1 + .../writer-request-0002/row-0000-image-0.png | Bin 0 -> 34729 bytes .../writer-request-0002/row-0000-image-1.png | Bin 0 -> 34729 bytes .../writer-request-0002/row-0001-image-0.png | Bin 0 -> 30742 bytes .../writer-request-0002/row-0001-image-1.png | Bin 0 -> 30742 bytes .../writer-request-0002/row-0002-image-0.png | Bin 0 -> 22702 bytes .../writer-request-0002/row-0002-image-1.png | Bin 0 -> 22702 bytes .../writer-request-0002/row-0003-image-0.png | Bin 0 -> 1454 bytes .../writer-request-0002/row-0003-image-1.png | Bin 0 -> 1454 bytes .../writer-request-0002/row-0004-image-0.png | Bin 0 -> 5580 bytes .../writer-request-0002/row-0004-image-1.png | Bin 0 -> 5580 bytes .../writer-request-0002/row-0005-image-0.png | Bin 0 -> 6457 bytes .../writer-request-0002/row-0005-image-1.png | Bin 0 -> 6457 bytes .../writer-request-0002/row-0006-image-0.png | Bin 0 -> 2562 bytes .../writer-request-0002/row-0006-image-1.png | Bin 0 -> 2562 bytes .../writer-request-0002/row-0007-image-0.png | Bin 0 -> 2634 bytes .../writer-request-0002/row-0007-image-1.png | Bin 0 -> 2634 bytes .../writer-request-0002/row-0008-image-0.png | Bin 0 -> 2645 bytes .../writer-request-0002/row-0008-image-1.png | Bin 0 -> 2645 bytes .../writer-request-0004/manifest.json | 443 ++++ .../writer-request-0004/manifest.sha256 | 1 + .../writer-request-0004/row-0000-image-0.png | Bin 0 -> 44129 bytes .../writer-request-0004/row-0000-image-1.png | Bin 0 -> 44129 bytes .../writer-request-0004/row-0001-image-0.png | Bin 0 -> 29927 bytes .../writer-request-0004/row-0001-image-1.png | Bin 0 -> 29927 bytes .../writer-request-0004/row-0002-image-0.png | Bin 0 -> 2780 bytes .../writer-request-0004/row-0002-image-1.png | Bin 0 -> 2780 bytes .../writer-request-0004/row-0003-image-0.png | Bin 0 -> 2800 bytes .../writer-request-0004/row-0003-image-1.png | Bin 0 -> 2800 bytes tests/fixtures/p23-03/passive_observation.gd | 6 + tools/check_p23_life_observation_evidence.py | 388 +++- tools/test_p23_life_observation_evidence.py | 231 +- 36 files changed, 4133 insertions(+), 66 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_MIRROR_106_DIAGNOSTICS.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_0b7727a_BLOCKED.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OFFLINE_REPAIR_OWNER_METHOD_DIAGNOSTICS.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/manifest.sha256 create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0000-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0001-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0002-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0003-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0004-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0005-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0006-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0007-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0002/row-0008-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/manifest.sha256 create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0000-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0001-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0002-image-1.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-0.png create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review/writer-request-0004/row-0003-image-1.png 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_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/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 0000000000000000000000000000000000000000..83c5dfa76f7a991913ec2fa0f0ed2a9d6f692f1e GIT binary patch literal 34729 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..53636cf383bb2a13564fefd7191ab29b3acb5099 GIT binary patch literal 22702 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8a339c9ec74639d8e7a370cdb9a4a644b3006ff0 GIT binary patch literal 1454 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 IM6N<$g6gl@GiZVciENqy>; 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 TU!`XI00000NkvXXu0mjfsGy{K literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..85e93242cff5fc7eba265f3744d67c6b3edd2b8d GIT binary patch literal 6457 zcmV-98OG*`P)PNkl1IQLl72a#u)AL$o-(s++?)1TkMMbk}NWplvVh>u~VVdOO4vv z$#I8!Sf|m6RfYz&sf}NhP2v%hb~$4|p301?Z@MTd$t1BbNE%7*(BZf&B9Dcmk!c)~ zMq7XGLSCO!nQ@5BFF4vcU}B_`GxNS{Q#AmtFz?&Y^eua@#^Ka1Ec?5i`mP>>d(0U_ zX1yyyIDLe`#okC6{GC3G|)1~p-wtlCgR$58pVrcux)N}^2pOTVJ zAf#jI+@u=mg#Wy(ztkG!S~lBR|sq}`!MI6 z_`>kb?sJs|)z?p#Rb-vJTrzaqS=(5B;M+r377>?5!7{M$p^a^|Cq=mg4l#vCz^7Bm z2iOE2oyucU*d!*8L}Me}eKv%NCuh)!YyxLLIlJJ8CRHPqOS{ z?KM$(1)0Nyd7*I`Yz|#qf1{uzmzlvN@TfR8iI~QKD|`~zg~B3`Sw!c^AeKeOa43ue z47swQqf>u4`zR%iE^d|JnM8z3p=Pj(&Xh3I=^a*GM|VdIn}=a?lNhW7HaD5UN}@1R zz+6-=9!pOGgL7Eev;!o#fnZSC2$hAWGYJeHg~-HHm^eBe$7Um39-ab?SqNAa!VH#2 zCzq4TVq@qWJVM7((H1m zkC(`0$Fmswxagu zGV9SUPun6>wmH9LVQPEGpmi~5Sq_?(JnE=Vvm7$c!>9r4!n9>B+_4z6MLeC6aL>YY z_d>`t?^iDd^wE%MCSVE%?C@~jXIlta7Q^P1kmbjqZSk%);xk5qrp2IX)}xF1JC=MM zE8dPpuOVj6*D>#hE==gyLC1p66!Gd|;02F<(QjV#TR}^H8~Ou}Vb!C1=+WMv)I9KL zS0@e7#ggB+=+mwQtSb|a$fSMIV_)%_A9@YY+$Q%PNK5Bb-fL-w!SA8bv7lv64I3DsXyMJ{U(kUkcfxVcTNZx)3l&0+#L3&2SGu#!uR23Un-m&2tml z<$!&Dq9ZbCUb<_jJ;|#PGU^138a`8!pH?GeOLBSD+042;ZgnpEMgglPk5wmNiG`dx z;eLrAy-jus9Kna~3q z(98$TF;TzqlRBa(W5?+K|I;1R4QHl6SB=E3F~tusEAn zFXYzea$(@c9A-;4rIpWUIl*o^&TGy(Sf8Daz7=vCbJOZGIjvdyTLfHbCI#Bp0Ct|9FfD}~3zH_8tj0WsR7kHEFl!4k&gULJ zFFbTP_uFd)hcEKk*9$W)n0d}A1 ziC5nwB#SeNQUUuMKmD}m#HpgpQztXe79Blbbg;7Ez#l}1tBV=+!q~=qOnok~s%Zad z(RbydqvfT?M52E!Dm+qB{B3E`w^hZ5Bm$a*&k&!WSLbu?1xyj20bGm@jWStwtQ-SS z!1P~$!szTSg`M+WYs71LbXQxSO_1g=s`;FAqVGBfY>q*Pt>4i#V0`q%x4t(0^ilXl z)ag~{+>z&w%8KmT%Z?$lb+E%WXt4}gZ0=6$u-!Ij8@9Ef)5qsD2sqbrn6Q@7Ao&cd zVQa)=SPGcXp&>#G0qZspN)z?jQLFn6u-xW7U9g5%16@xh4b2dt9A-_y!J>li^sWKT z@PKZ_t##R=&*v5%hM%n6d-ve>czf0@dCrJRWYAwT+;pm4J<6e8#c;pEHKcG4sN932 z{g%2D`y2TC8?!mpdE8~cX(?<$lMbWJ{;HOAf58x4h7huhKjE{Y0 zNj4QGzFPQAsVGY`I;e8@!vF?X=i|QUe2!GWfgn|Zse#6;|G4J8kdv%CBq-2tOi|QN`h}2cA!uCm z+fn48@u9~g5%9!<1GU-TloV&nM+W6Cr+Q?-=o83NOmIhzX0vb+}{9oY=;M?&yH>}e(jS+aNzbit$;hOLWF=NH!_3+qcLP&hypffEu@fq?rr=(v3Rww|dm>E}h#x zyB=NKMD^l<+tYSpT$Veo6m=MC-o>V$27(d&d+gM|6mW7rN4vn7XDDLSThSye8(!qsWG-5XwCUVOH?w6*;5 zfh#P}nrP+UX+LSuURT~4RNZkZZ`xI(HqEF_HPQtO?+-?(J`gFOMR`}OC$2Q;<;1Eguokj*?~vkXF}w%_iOHwvgQ z^{~qRfd~aHkSiBGW~kJlu7m8k;_rmlvp(HJzonXwt`69)FBPQs4a)j^n@5~2qrFYH zx*JFPS_gWXd_6VeZFykoyNaAaO@+(Z>h6=dz&TEZyQd9u;X5 zS(m)DUr|1!yErVbkmQi+b6^|hLLP!~p?HNN1PY_2?E>v15D-MC?bDu)XxK3e5Fx0M z(Tetd2o{u>IrK5wLs$ zg7Ek)Q$EYetaEw-eF{-o^ypSSW~dONKHZOAQ(aD)m0+Mfr(abzqB%XLIWsIPxuvZbR-Woto`Fy`<`Kl%B&m>l zHJ<}zAOOK$84X&|LOx{uIBV9xK&VGGYzuw_Abq;mg2W*|g7I%+q zsO>aFV#Ijbu08AQZ5iqZ@3%V#l$`_0u70i4X&UR6-N>fa3RtyJ=n3hIGuBzCbAlGs zGePTLBLv|I`)qLV1C(iB*YxbDe|BQhJ314b^myE!kUQWRIage8E-(9XUS@Hz0ICjy ztJ^TtV;pg*21j%*m)_m)?9mUYuG{739r{Z;W3{e2w^VNSB@@3w@c*>2>3{xe zcH_m&+Sbh0>*%BPxo0o$Z9ZRpwK@08M(F($?;js~->nB+ zOg-&@WAaq!RuT>qM_|Sxv^WASj!4;2JmJ5I7LU-k#e~Lj2pet^5jy;2AAuZ;kHt{F z*xdBIdp7=Z%k%uj^o#YWcN_EXwq{g6N-v0BCW4~-o|M+x%^VPj4ThaBc<)=?qe%cI0 zJ@RPC2qipB-wsm@!JQh=e569}mZrK(NEj`8ub(LwHhZES|K7 zf}9keEBK_mx(ol4KE6d;)}1{mIeYQ)N!i&fRq0Vn@$W6AM-4?snkodO)V=hCFTP3o zqNV7Fq5Poa?8ziNEgnaL#Sw>)6Yyj_K5@?1yXfm&@uL6w*r|eI&`l)V7xKCiF$u8< z^)o8|c*A*A^%uWy09^(hufKvyT6IENl_^1$U00PYxx%lza-#m?ca7(-UN6)Ze$!ie zxL?TW5*_NOI3_-ypFqVR*hEfJqU=<@q4cP@QWT4$$Kol#1T@J6Y6>oXIy^M*bKHl0 z-Dg7ocLSz|JvHp@4FyIa>+Hf)zF?5E>Mw2UN+znSpe*UN-^7=ElJu%Ta+NQ=k}17> z`o`JnbE1Yafw7#g75&~=w!gXb2rKEc14*ABj{jU&ddyUq-f*^XAAuSR*?~ZeLx}Ns zLJ~RYcDR2g*!f^WcYo6Gz+;3^!CuxC^g6@gaj;t~mGjR;1cnhxtFme@ozk>as9Qm0 ziq=Z40#vGKI-{4v6&_aV+A7q|LUC1rw6>t8<_jVv37g7DifgO*uA`J`Da$Yy9k!K! z2h-P8dZ;^}-grjvHG9FHfoKaOdiju{?Inq%HP3qRkQ z-`H49BH_Qp6ThI+a_Vm6No$1S>y>I*rLz6BqOH8WdFOgo-CiMUI-_cQAeh zb80T<-nb&Fu8yNJQ}GBTC9%Fzs5o^t;`gcXEmJ3Z?4R1DbKlZx*~~sAQoZl!;zDT3@nlP6$L5PN^2zLVm=ir z$tI|i1dNzBn?XqMFSeF2#H77Q>epoA=QJ9`>32lZF5n}Pc9Y1v5F%&~kqp{RAnm47 zza&w);uA^u1OyvLBqouOL?k5+M8U)piNt-WM4*W$ zGxlOh@i@vCWI~auzOnUuV;&2D;Cx(jGA-uaX5jVK^s9||*dJoCvGG*oYYJr-g#sL~ zgT{jOh~z{(F$p21;3=`GNIZrdi=)O9S$px!cv9MKJZCR*Ace$C!XzY8@n2D~yNHBc z7)mUe2?p4Qkm9jK=md-$kHukdghYh02i^E+$z(Qsc9F70+Hk%uiv~2}6O3yG+?b!Y zysy{%Z#Je#$k$kMd?My640aa?!RCj&D)J?r{5vZ7GYa)H3gv4i7=W~gLH&|K{G3XJ_4_YmBD{qk zeo3K1fd83JEmXBMwO*;qrZ?vD>IG?4Ih>gHPse`Q@V$NFd$l?9`t8FHe|+@eb>zdV zg%59+KD>#1c)R@J{pzo8SN{E%M}Pc}wY9eotCZqOS0;FaPJy%W(b6yVXB`(qFdq>+98*|F&7FXeyJ}m8irO+Qt%PLz%L% zLecz>3)+_`>w&mbp(>LrPAQZP(n}H{yP40C{lKcp-5&$70h;X^!}oSG{B|q!b}R61 zGlW)m8=-fbVG#O!bNZ*X(B0`f<+V9KUfbv$pF0An_%>^b#zBVx1yi)m)%$t!p_i&Y?<$R5%=8&13z4UWyvw TU!`XI00000NkvXXu0mjfsGy{K literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d28700dec8b89de94aaa992d64c705f02c30ad49 GIT binary patch literal 2562 zcmV+d3jOtoP)dVQ=X@@$Rq^&0cV>ioLOIyd?{bDK~@B}U8cFyY3-i~UaOS{pgwR`lwptapAT-_AAgL0=| zYF)#q`a?FWOOIWf`_$spS)9jb_Cu>j2MsYfH5RuP`ux&%J*swZD}y&p=>9_kpT4lg z>^e5PPfV`UGsoC)`1MA|A@t~N+w9OhIn=?k$*F~(R59@IzH4$GVXM|oC$i^y^PbCZ zz@tXn;q|cM4_P7!H@1qkm^WZ5HxhtgavvL9T8PbOm_jenR%sUZi929yUFRF^RJDHj z=})(}Z(99fad4Bqd#C{t+=b(=MmJ+}99nVTx@Yrf;Vzc;A)sXL;_mV4_HDC&U;E*6 z|Chgixc{*;eA~QxcNIywZC>3))K>3itRC%|{lMZr2BduLGM@9pK=fEQI`{oyn>h5d3C3bL$gOaHM7o~U7j_&bVy!jP%PFXk+`MNO=ZfV zP}KD1O(_sIxdUeGUPSHfegSedD0YUWW-r&ft9e6a+|^t6QBS)MML4Q<)5%g$Z$B`) zG{~#fcLL)g?xx6C zP9P4q4w*%YZ|=%=?@@15a(a$U)?K^n$l=yJJT_rO6OQYC{^~mO!~4#}xV^7__JD>1eYtL4r{nps$)!tI z!ktkWV4D4Wdsu|D2E|-GdTKqi`_B+DP=!+NU;p_zmJ2}8oVfMWsJip0^T(g>d-u)u zpwjA>Zys9jzdQuuZle=Dc4BZEjP5hNO=s~KkURA4-DA({Wegx_V8?CBP(jHEG&|QU!|J1CK@A3t5R=JfVgx`v{*_7lpPx`%3 z!4hwC3iX&!!Qsm`A;)K`>Qx#^IWnjly`s*%Pf9kq;tipafYzMtP*N6B;0aj;3J$jT zayATsisEhaC?yrn2C|Jrwi+uoQ*S>Ga@AO-6e~3{u}lz8NL1wGPXot=-s;npTZwcj z7)~NFY7FzKWw&=%Ju&Y67KN1iY-FA znW{$Ld>o)1a10@ewke)$%XwjH4oiVFMs7!SD7?9^gW+x;+p--JiFTeFK#Gx+2jg~d zB9CT#m{FPu}d z4W4|n*oa@Iy|JtxPUE@rSoR{3526ABDaRAAHM%9qHjghS61nisk&-9lk_s|iP5B~j zfK==Vdv^`g6J6cCtrhF(LNysq1#pH?k{ksWf(moxob!a&5j5AY3n;SIbU-Nxh)~Td zw6p12OsFPf#Q??!yx<9&v$ad%Hk&JX?GBl+a+&qjd%09OiNnm+A5tdQN&$5FDrNL&HP&5Hz6l%j6V03hBp{)| z61mHAGtUv4Rwe z39gjOR`57VicnB&8JCc8!Xb}%2l4HQbMhhZyyOK z!2l$f50$X_PhrF`x;J?s1o`C~F%+rLj3Ze*1rHY~DKSQiP8KUEt@UVEuXctkV4cow z<1!V*>_v0_Y$KMcM%dy%N_M#Jpau2E5F(&t@zNc1AZPG`Nw^VwIqaHdb(Q?Nnn>_Fp<=YJot)6{&@SeF;lOps<9DX!Pq=zXby(RFZ{88g0Vg zCd=JIrjY`2NTM9Ym0-dHB+^|Hg%znd?O{p2&vOROASh{PR4KKSFmQ$=`mWeYAXHMu z<;zJpoSIo(m|L3T3O2?SR}g`eS8LqeJ64{UG`_G6bvj*+$kJ!ttwDa5u za#bCQDaj`K1)tz^<-U**zD}1SAogajX!GjfIiz?4FM=$=6RUWDjflB2Hd+qUilFgx0<;)lv^1sWkE<=(n!rHl@DIurZ6h6QO2k4OfC>+A z4u%7Hj$=g`rCZ>6shR?k35+oS2^#@SG^#W)AjsoHqa(oZFt7_(5UhaJu>y@3rQ77c Y05r&U%M-9~&;S4c07*qoM6N<$f-@28z5oCK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d28700dec8b89de94aaa992d64c705f02c30ad49 GIT binary patch literal 2562 zcmV+d3jOtoP)dVQ=X@@$Rq^&0cV>ioLOIyd?{bDK~@B}U8cFyY3-i~UaOS{pgwR`lwptapAT-_AAgL0=| zYF)#q`a?FWOOIWf`_$spS)9jb_Cu>j2MsYfH5RuP`ux&%J*swZD}y&p=>9_kpT4lg z>^e5PPfV`UGsoC)`1MA|A@t~N+w9OhIn=?k$*F~(R59@IzH4$GVXM|oC$i^y^PbCZ zz@tXn;q|cM4_P7!H@1qkm^WZ5HxhtgavvL9T8PbOm_jenR%sUZi929yUFRF^RJDHj z=})(}Z(99fad4Bqd#C{t+=b(=MmJ+}99nVTx@Yrf;Vzc;A)sXL;_mV4_HDC&U;E*6 z|Chgixc{*;eA~QxcNIywZC>3))K>3itRC%|{lMZr2BduLGM@9pK=fEQI`{oyn>h5d3C3bL$gOaHM7o~U7j_&bVy!jP%PFXk+`MNO=ZfV zP}KD1O(_sIxdUeGUPSHfegSedD0YUWW-r&ft9e6a+|^t6QBS)MML4Q<)5%g$Z$B`) zG{~#fcLL)g?xx6C zP9P4q4w*%YZ|=%=?@@15a(a$U)?K^n$l=yJJT_rO6OQYC{^~mO!~4#}xV^7__JD>1eYtL4r{nps$)!tI z!ktkWV4D4Wdsu|D2E|-GdTKqi`_B+DP=!+NU;p_zmJ2}8oVfMWsJip0^T(g>d-u)u zpwjA>Zys9jzdQuuZle=Dc4BZEjP5hNO=s~KkURA4-DA({Wegx_V8?CBP(jHEG&|QU!|J1CK@A3t5R=JfVgx`v{*_7lpPx`%3 z!4hwC3iX&!!Qsm`A;)K`>Qx#^IWnjly`s*%Pf9kq;tipafYzMtP*N6B;0aj;3J$jT zayATsisEhaC?yrn2C|Jrwi+uoQ*S>Ga@AO-6e~3{u}lz8NL1wGPXot=-s;npTZwcj z7)~NFY7FzKWw&=%Ju&Y67KN1iY-FA znW{$Ld>o)1a10@ewke)$%XwjH4oiVFMs7!SD7?9^gW+x;+p--JiFTeFK#Gx+2jg~d zB9CT#m{FPu}d z4W4|n*oa@Iy|JtxPUE@rSoR{3526ABDaRAAHM%9qHjghS61nisk&-9lk_s|iP5B~j zfK==Vdv^`g6J6cCtrhF(LNysq1#pH?k{ksWf(moxob!a&5j5AY3n;SIbU-Nxh)~Td zw6p12OsFPf#Q??!yx<9&v$ad%Hk&JX?GBl+a+&qjd%09OiNnm+A5tdQN&$5FDrNL&HP&5Hz6l%j6V03hBp{)| z61mHAGtUv4Rwe z39gjOR`57VicnB&8JCc8!Xb}%2l4HQbMhhZyyOK z!2l$f50$X_PhrF`x;J?s1o`C~F%+rLj3Ze*1rHY~DKSQiP8KUEt@UVEuXctkV4cow z<1!V*>_v0_Y$KMcM%dy%N_M#Jpau2E5F(&t@zNc1AZPG`Nw^VwIqaHdb(Q?Nnn>_Fp<=YJot)6{&@SeF;lOps<9DX!Pq=zXby(RFZ{88g0Vg zCd=JIrjY`2NTM9Ym0-dHB+^|Hg%znd?O{p2&vOROASh{PR4KKSFmQ$=`mWeYAXHMu z<;zJpoSIo(m|L3T3O2?SR}g`eS8LqeJ64{UG`_G6bvj*+$kJ!ttwDa5u za#bCQDaj`K1)tz^<-U**zD}1SAogajX!GjfIiz?4FM=$=6RUWDjflB2Hd+qUilFgx0<;)lv^1sWkE<=(n!rHl@DIurZ6h6QO2k4OfC>+A z4u%7Hj$=g`rCZ>6shR?k35+oS2^#@SG^#W)AjsoHqa(oZFt7_(5UhaJu>y@3rQ77c Y05r&U%M-9~&;S4c07*qoM6N<$f-@28z5oCK literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..282dc003716bd3dbde2cd733617d0832baf97ff7 GIT binary patch literal 2634 zcmV-Q3bpl#P)HlPoE2>q5G3a$k|Za|pkM%Fx9v>NOn1-jo}Kx!^QZQ? z(z9pxoH}*tzSLV)Poeu~XmWG=I2mS)&o zHj6ViyEwhfVJvWG<~R)Y@{48e^xUiI1rD3J@Ny9fOD|u&W;5o$Sz4W&TV~EK&fq83 z;w)>4F~7oMEzLlOy~1QK&$3=ImtHS`Lt%bp7G@VXEa=Qb{}mH75!lOYct;E~OUyY~ zSZ1(aF<^)D-O?Oq7IvV5D60iN3M|FD#sc5CAxgAVS!|ITYre z{W2D&r@mQa&VRdL4O()|%;2tnHma9vhv9^yJ}A1whMT)aJma;@kp{jL>ByKVm6L?O_*DB1%%uCU?2`_>)Vub<|RD^X|2 zlsh@Pc^Zr#`2Q>CycYKdiior{V)Vz&ABT;Y9dz)Hm)L|;dP^bS-ZUJM3OF7 z&}{YWc*161^uQS~w9m_hddliG(8URvyb+5tV2o$|-SdjWZvfy)3!pg3@}VbYKk)A# z_$=rSZ^Q~Cu7JVm-y7X`Amc7Fg)20LOqX|UU&M5A+rqicyNeS~c>ngp8H98pI2_kb zFU#*gUJS?e{$+La(0%$eMudn2J@VWUK)bv=yTV4-q4bX*uReXgy*MrY^7-cJ?+@qK zjo&_v2E$6Bns5h9l}75~rr8~q&aP_1Q3Eu%YM!)nHn%YlwL82bkmBZJi}4zAAkJ#^Ow8Jn|35-cX@a3-=CJMvD0Ds z>Eq@3RlU*8C9{FfSqUDUev8v@^@i=9kZo#qmCIaVi?jlTo+vky(IczJxs}WZV&$mA zzgKT2Y|bsswnVDoHBSqK>|4c#K&1RZta>BU@)a9GOo&V?l&JX<^}5Nv*&YDJC*2Ic8)m92VlRK12QIDEUu#SnJto<)uD(Cv2L9$g{%8?jadU!;OJ_07^eYn3fl z^W|Hj<67)6?Lop2S*D&z6{ET8AvO@HUW31relA^(WGjd1VmMcgWh+PdTB6#{N>svd z+}k^EC^jkJ$<2dc4}rVEsgr&T2a?*YPRE_jp8X z-W`DKT~tbqL@;5;`qb@7%k8vSCnS_Y0H)Et;4I8?S%}!^+8sWeji1i3))V=tLMH`p z4vN@bM$a~-S%-|eWy&#t%vV#{GB~=fCQ%ToO_@YRiIgOzrV1yyUDK9GBO*4$LKQ*E zg>tR<@CX~OX$#V;0$>zcN~R!46)Dh)g*p+T6$#WNp(a!1WTuir*;|7es?i;_u&$9c zkj|j3^{b?c(3>?I2H7N-P||4+7*GlC;Q@qfE!MlL0iiEut@W}Z^%^Q9(h_2=7#Bq< zf=0`JJ*Uu#_C1Det3Hr)A9(igBpw$Nm_5u)rI#(X(*ni!$}KXKaO0IjXCk0N{Wz3B zql#cftR}%l8cLuP6kCN|^EOY;2U=hNY{jPZeB4D#SI>&QaTD#W)=Q;IQm&Utca%Ie zA=CmY(HnI=hi;TtrlU@JB}}zsgKD0ilzT-W2j!a?TuJ1^K%&~FP<%8w-Vx1?I1smk zf(gg)w$(T*K!A=={vF;(gi5JNi3WW~OwDkYn9EF3x`sW06vm5@eaH4&lg{GAljX&7 z!ZY>!(3?l^&hR7>@^xt9CVDlRiCjO84URoDA)P!_Nlg@*`5p5%siD?YLa|;(C<$lK zX0&b#m1~5C_tw6ds>G_DLZer0oHtY^osjlRAyZqX4J;B$&O_>l<1S#+fog~z0 zOhbM}D^+1-tqW9Z;MlHMo>Dc1)go4{JrkS>G27sDQj~mp#c71d_k<%FnO-bV@(4AZ zNU25);!aEeVaU?XBuEkMg*wIXOdCqRn zrx%85UA}>I2t;xo;s8`wfN|x9n3O%oiZZJBltt^fB+^ORhkGO?psM?_Z13hPV6n>4@^8f$<07*qoM6N<$g7O9$xBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..282dc003716bd3dbde2cd733617d0832baf97ff7 GIT binary patch literal 2634 zcmV-Q3bpl#P)HlPoE2>q5G3a$k|Za|pkM%Fx9v>NOn1-jo}Kx!^QZQ? z(z9pxoH}*tzSLV)Poeu~XmWG=I2mS)&o zHj6ViyEwhfVJvWG<~R)Y@{48e^xUiI1rD3J@Ny9fOD|u&W;5o$Sz4W&TV~EK&fq83 z;w)>4F~7oMEzLlOy~1QK&$3=ImtHS`Lt%bp7G@VXEa=Qb{}mH75!lOYct;E~OUyY~ zSZ1(aF<^)D-O?Oq7IvV5D60iN3M|FD#sc5CAxgAVS!|ITYre z{W2D&r@mQa&VRdL4O()|%;2tnHma9vhv9^yJ}A1whMT)aJma;@kp{jL>ByKVm6L?O_*DB1%%uCU?2`_>)Vub<|RD^X|2 zlsh@Pc^Zr#`2Q>CycYKdiior{V)Vz&ABT;Y9dz)Hm)L|;dP^bS-ZUJM3OF7 z&}{YWc*161^uQS~w9m_hddliG(8URvyb+5tV2o$|-SdjWZvfy)3!pg3@}VbYKk)A# z_$=rSZ^Q~Cu7JVm-y7X`Amc7Fg)20LOqX|UU&M5A+rqicyNeS~c>ngp8H98pI2_kb zFU#*gUJS?e{$+La(0%$eMudn2J@VWUK)bv=yTV4-q4bX*uReXgy*MrY^7-cJ?+@qK zjo&_v2E$6Bns5h9l}75~rr8~q&aP_1Q3Eu%YM!)nHn%YlwL82bkmBZJi}4zAAkJ#^Ow8Jn|35-cX@a3-=CJMvD0Ds z>Eq@3RlU*8C9{FfSqUDUev8v@^@i=9kZo#qmCIaVi?jlTo+vky(IczJxs}WZV&$mA zzgKT2Y|bsswnVDoHBSqK>|4c#K&1RZta>BU@)a9GOo&V?l&JX<^}5Nv*&YDJC*2Ic8)m92VlRK12QIDEUu#SnJto<)uD(Cv2L9$g{%8?jadU!;OJ_07^eYn3fl z^W|Hj<67)6?Lop2S*D&z6{ET8AvO@HUW31relA^(WGjd1VmMcgWh+PdTB6#{N>svd z+}k^EC^jkJ$<2dc4}rVEsgr&T2a?*YPRE_jp8X z-W`DKT~tbqL@;5;`qb@7%k8vSCnS_Y0H)Et;4I8?S%}!^+8sWeji1i3))V=tLMH`p z4vN@bM$a~-S%-|eWy&#t%vV#{GB~=fCQ%ToO_@YRiIgOzrV1yyUDK9GBO*4$LKQ*E zg>tR<@CX~OX$#V;0$>zcN~R!46)Dh)g*p+T6$#WNp(a!1WTuir*;|7es?i;_u&$9c zkj|j3^{b?c(3>?I2H7N-P||4+7*GlC;Q@qfE!MlL0iiEut@W}Z^%^Q9(h_2=7#Bq< zf=0`JJ*Uu#_C1Det3Hr)A9(igBpw$Nm_5u)rI#(X(*ni!$}KXKaO0IjXCk0N{Wz3B zql#cftR}%l8cLuP6kCN|^EOY;2U=hNY{jPZeB4D#SI>&QaTD#W)=Q;IQm&Utca%Ie zA=CmY(HnI=hi;TtrlU@JB}}zsgKD0ilzT-W2j!a?TuJ1^K%&~FP<%8w-Vx1?I1smk zf(gg)w$(T*K!A=={vF;(gi5JNi3WW~OwDkYn9EF3x`sW06vm5@eaH4&lg{GAljX&7 z!ZY>!(3?l^&hR7>@^xt9CVDlRiCjO84URoDA)P!_Nlg@*`5p5%siD?YLa|;(C<$lK zX0&b#m1~5C_tw6ds>G_DLZer0oHtY^osjlRAyZqX4J;B$&O_>l<1S#+fog~z0 zOhbM}D^+1-tqW9Z;MlHMo>Dc1)go4{JrkS>G27sDQj~mp#c71d_k<%FnO-bV@(4AZ zNU25);!aEeVaU?XBuEkMg*wIXOdCqRn zrx%85UA}>I2t;xo;s8`wfN|x9n3O%oiZZJBltt^fB+^ORhkGO?psM?_Z13hPV6n>4@^8f$<07*qoM6N<$g7O9$xBvhE literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..88da40154a5c9a467d68cc14064dff1c2f7edfd4 GIT binary patch literal 2645 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 B6J7uS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd1e7937485732211a1175cdf41450a11eb4ffff GIT binary patch literal 44129 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 B6J7uS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..c61ce2b808835b6e4b0f1b3e1376e887761cefda GIT binary patch literal 29927 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 ih;X1-d!|^5UBS00000;51jIW zs2%2%LozumtA_+q%xNV&DTWE;BT4ZQLOao~)gm9olVUN2>o5?tSV_r_RD$}yVAXgsAb458sYt1+C;(JIa9f=hPc=KcG^C!L}Q#hJWI<6N5H(q*Sk za73vr(uPE-EKNAIOvlcYp}1SjEHODdLJMT{+dB$dkLYLEX$e06>?bMIR3#mYb0<8yapn_5|q zoNBHYJQ{Ba$4^zoS3;r18CWnj9g5H0U!1S`xJGiTnoomCCa+pN@rXE8vPHoZWUnIH zidJq`)mJU&Q?b=>aw8tigp%t^i@A8s*^{iL@E(nI%Tk#JgrX(L79&}%?Psfch0N%T z>QiN}%v-Eksdn^S+TP8YK0&c4|J1FM=W0baPs)kp5fH(pT(ItwBQ5Qx=Rza5{5L9U z?7-ZxF18l4(yJYP>da!1eQ^_}$ER&wXj%GQ(BpMU)^y|t3wO6Q+$ zH($LZdq{0#-Q@iV)h)a?!Q}f#`pPWT3U2ytUo1ZH>g~g|t;D^^*vRw%@v=r4zxyJ0Yw+^U!+6u# zN^dz|Q}3M%kIEKjxD`W}EK4>`sR&GPX$<1$t2Sea$-sE`$UPJUMoLi9HlI7D(FK@Y>me-Nr2^n5m_aY&p z=HX@2>8<2bD>LOjels7o%wp4aC|aqvQqFBB6w`UOr6##Nol1@Gzg*2`X2wRZ=eHN% zyxX0RO@VHzC8_Yl;EO6yR&uop^9U+Qk> zExzk!dpI$*l$wdgCqmKjXlnLebh^*qtCXpNEl6%2By4Ox@$0{TYH2%tspG6-a)yt{ zV5u!OoICmY-Ck-nm`LACrRS5G`!C-({@mXF|2rB%Pf2<>xowfA@JXJaVGWW4fuWomgjQ!@}HWFZPnD z*l@t#jtq}Y_N6x#C6iUXD%hlWyca0=D=;ubeP6rIHa2;2(48v##R;p24zD$e0^i(?%pc!e0;9@@$>bM&!41K3hBj3 zqQRYPJccB6eA?W1>oT!ut6WBvDqaoYM%@aQJR6!_-Atsj3oF^VpZ@-OIU9UVOe14i4tGA}y`; zxBQhJ?A^>p#5`iK zO37M27K;WV_XiiF!?DP4I64xIPo+1af=+{?tydfO-)tYP6ywS1hO(HYJV(G(bu%-OQ2i*RFj^5(!u*XE;CDi>*PI}6r8c2WODC|o%r%XcYpgzE-GoR)s46eU9bs81Kw1M4jMg2 z%G8#tP4#E01JeT++RnWG^YiKX(forI1TvKiq6vd7#u0L7dT?T4Trz1{cS4hI-RXi3 zq2Gd#*b4&#YB-z@R-dWHB*1IB?(@(ZV^@<$`&kv;I}?5RdT;XXxbK)3<)jugOhpF* zT9o&*7ms^~dLUYmleO&(L=Kv~vAepo6^AGg!erKxIBslq7~11LqAssRvEbSMPFa=S zJ9NG8cIWM}-X||MbK6N&Y$6ksO&ZBZz3?WWO5jluMkSb2aiIsG0{v5iO|6ZUwdE~W zn*&oroLti0eZIB3`AYAl{crb&riPFZ7seYj8=D=U^S}A-P1BVY*`o`Vh8>aTa5{3L zsZK4k1>NE_1xdR;&|cqi4D*>{NexX6<9DYM+2!@e`P{?xno~7k0qYhKsiQTOiibe7 z0QlMdE^-1nMISzK?gXPah|aI?tk#{aJJZ_OGuXNLB-eN6Q_)~hw~Ed8-tOw`Yj3@N zzN)Sqf1z3Q4|iv`m%9h9qtM_OhMWRg+Tfgo#&l7j7LwzU3L<9mFg&7d!b{esX_GDx z&*UNC=r_Dg!w*yx9s!j#39TzA4D2R&WT>BuNTOwdKM1AdjdMv<+C>azW_{bmJgHT8nZo~s# zz-{ESkWzz_3b=&#Xi>mLlqnP|lJz%Rg2p#U3ARV|!n^Q_In zD1*WHs(3BNP#HsGECm8fTOwfdw$2zj?l=lwlxg2a=C>^fJe~b0Ox^*VGgIj8oU%N6>i`YFykvG1yG}~%q&C=H}nAtEPxf_ ih;X1-d!|^5UBS0000*(P)8ge zMQDsecF`(n7i^S5{HB?5U`qpBr!+Q~w`Wlre~HVLyi)IQTXbtNl3Hl%sVgYerWS`J z&AGMXzg}C)ngXLSSFYtpHka2T(;%ziX@7KnHL_50qlhf#NC zRawC;at4omxUixiu^)@1{E6LAcyl$r7m1|9r8T9D#)5KM=R}W$JV?-JQFHN{a4p+T3G8Jop}Iq{_U4rnP75fIhLGG?5uf9 zJhGduuDv=o*(sxJ&2VUjOIDrZGmon4%aF;ncHP~5me_e3J$#-xINJRAmmkyn8|nSc zgX8^%<_2Vl@)QaH^J6@}3>Mpfx7n*>Rx1rvF%;81LQY^uHdx*6S=>v_~veXDSD zd!f9hFu$SzqFYs48jQ~cLKAC&vGve|KQtXqu9Q?30Jj=hWE&<2T_K=niBS@*dt(*RNTbwVGz4% z*)1ZEB$@)C#>^L@n@esVzqcRMm0VSKigKIZ@{%jh8e|P?#*2B040FLV~wY`}X6D(>L3}!1%}SPv5>hN~V^6_{#|( z_HJh@K(6Ca>w0817GDTPX4Zm}!HuP*;C$~$ujrOpodZw-4y$WF|K-0w)i+h%xnIrc zG$5Qe1?j*MD52u!*spW8b_2k3Ln%3LkC!vdk8*?6wRwPgYtcDCCa})y) zd0pgmQSxAjW;DI}&;R_GN(YNeP;z79??1o$_`|E^z-V!~DZ5>J&qA#aDwmeJU%!q= z!^6X4&DFQ_M`wCBGGVAe(XBA5C>l5m7>L3=uOaiY5DX5DkGHlom94K0ulmRCHCM!w z6SZGdKyJbGR3-%IRTUe68LKu3t>zO#Nk5p#$S%Nh#f^gFlkIqNsp@v|(dlkgT{*BC zr*TlgP&{STMS|*Vd05-s-vqhEmnADk9>t~bq#@C|G`F_2m5C>|{Tu1Uzy17fV|y*K z8H#U57DEf1>M#qqfBpMULxXL-J@wBH0^#W7#B_HQ+qhwo1sz9>CAOP`)5 z*Zl*Tr-6pX3aH-LPLwftN|%717(>h=Re{1RgBWj~@ZC30-+rBHZMr%$)#39p>+6H* z?djCsnwqCjqHQM=3I)dktApX-NGLG89vlxwW>dRiP9{W+Y;37HJAYOHc#F^8s4mn@ z`pxJ#@F-yuoQ4*gQHz7tvADgv5C?4 zs%!3*)zQxGdrj?k!&^~W7n~~Z(mC1`vz(HPkicBtlP~$#hbN{UHr^|H_x^BYZ48!y z(8yY364U{}(wQ)9t_LRl>!aYd^^MK}6GDCV4R=33J%m(42?gSFS1LV#YNScwWO!t5 z3~-@d=AeosWN&M$e|!EiwHJe^&jsgcMS!SN1_uxjWGFChWcty-=!1hVV~O-y{TJ2H zYZoV1i?L{2*f~f+Q^js}?|3WrGy**wAu_cShvz01hBx=Z zMxK<|0p4a`ogI1#aSznIw&u>d#?IQNp1Wt?pAF6oLPDHkt_00SrzRIi&i{CR ztEoXSF{{a-5%Rpg8Mu0@O!DX~)@WITq_y=oS2Pq+*a7uYd8=}Aem0s;>>g#dzuYak zRstvhBtxVMO7cY$5ef?qzdAdHoWMxHXw=o)S4q*1FtdBORaR4Wv$48spnd^pfZHu^Kh1YA%~;U7X!Ha$v{{3fAvzJyX&T@MLdm;f ze6uko1LK=hX5lbEi;yVbY7PUWS<71m$jr(B%!6Jwwj*-&#@QnT>!HopH@m0+Uge?`^j4%W0=EBg;G*XdBMJ6@T>Yo}1ZZkS270eh2N}AM2)=d)z zLwY2KF3_e%C@?(nkZPn0)`+fIehRwmK#btiU*(P)8ge zMQDsecF`(n7i^S5{HB?5U`qpBr!+Q~w`Wlre~HVLyi)IQTXbtNl3Hl%sVgYerWS`J z&AGMXzg}C)ngXLSSFYtpHka2T(;%ziX@7KnHL_50qlhf#NC zRawC;at4omxUixiu^)@1{E6LAcyl$r7m1|9r8T9D#)5KM=R}W$JV?-JQFHN{a4p+T3G8Jop}Iq{_U4rnP75fIhLGG?5uf9 zJhGduuDv=o*(sxJ&2VUjOIDrZGmon4%aF;ncHP~5me_e3J$#-xINJRAmmkyn8|nSc zgX8^%<_2Vl@)QaH^J6@}3>Mpfx7n*>Rx1rvF%;81LQY^uHdx*6S=>v_~veXDSD zd!f9hFu$SzqFYs48jQ~cLKAC&vGve|KQtXqu9Q?30Jj=hWE&<2T_K=niBS@*dt(*RNTbwVGz4% z*)1ZEB$@)C#>^L@n@esVzqcRMm0VSKigKIZ@{%jh8e|P?#*2B040FLV~wY`}X6D(>L3}!1%}SPv5>hN~V^6_{#|( z_HJh@K(6Ca>w0817GDTPX4Zm}!HuP*;C$~$ujrOpodZw-4y$WF|K-0w)i+h%xnIrc zG$5Qe1?j*MD52u!*spW8b_2k3Ln%3LkC!vdk8*?6wRwPgYtcDCCa})y) zd0pgmQSxAjW;DI}&;R_GN(YNeP;z79??1o$_`|E^z-V!~DZ5>J&qA#aDwmeJU%!q= z!^6X4&DFQ_M`wCBGGVAe(XBA5C>l5m7>L3=uOaiY5DX5DkGHlom94K0ulmRCHCM!w z6SZGdKyJbGR3-%IRTUe68LKu3t>zO#Nk5p#$S%Nh#f^gFlkIqNsp@v|(dlkgT{*BC zr*TlgP&{STMS|*Vd05-s-vqhEmnADk9>t~bq#@C|G`F_2m5C>|{Tu1Uzy17fV|y*K z8H#U57DEf1>M#qqfBpMULxXL-J@wBH0^#W7#B_HQ+qhwo1sz9>CAOP`)5 z*Zl*Tr-6pX3aH-LPLwftN|%717(>h=Re{1RgBWj~@ZC30-+rBHZMr%$)#39p>+6H* z?djCsnwqCjqHQM=3I)dktApX-NGLG89vlxwW>dRiP9{W+Y;37HJAYOHc#F^8s4mn@ z`pxJ#@F-yuoQ4*gQHz7tvADgv5C?4 zs%!3*)zQxGdrj?k!&^~W7n~~Z(mC1`vz(HPkicBtlP~$#hbN{UHr^|H_x^BYZ48!y z(8yY364U{}(wQ)9t_LRl>!aYd^^MK}6GDCV4R=33J%m(42?gSFS1LV#YNScwWO!t5 z3~-@d=AeosWN&M$e|!EiwHJe^&jsgcMS!SN1_uxjWGFChWcty-=!1hVV~O-y{TJ2H zYZoV1i?L{2*f~f+Q^js}?|3WrGy**wAu_cShvz01hBx=Z zMxK<|0p4a`ogI1#aSznIw&u>d#?IQNp1Wt?pAF6oLPDHkt_00SrzRIi&i{CR ztEoXSF{{a-5%Rpg8Mu0@O!DX~)@WITq_y=oS2Pq+*a7uYd8=}Aem0s;>>g#dzuYak zRstvhBtxVMO7cY$5ef?qzdAdHoWMxHXw=o)S4q*1FtdBORaR4Wv$48spnd^pfZHu^Kh1YA%~;U7X!Ha$v{{3fAvzJyX&T@MLdm;f ze6uko1LK=hX5lbEi;yVbY7PUWS<71m$jr(B%!6Jwwj*-&#@QnT>!HopH@m0+Uge?`^j4%W0=EBg;G*XdBMJ6@T>Yo}1ZZkS270eh2N}AM2)=d)z zLwY2KF3_e%C@?(nkZPn0)`+fIehRwmK#btiU Dictionary: 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) diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index c582983..2f4a0ac 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -155,9 +155,9 @@ def image_kind(data): raise ValueError('unknown screenshot byte magic') -def inspect_conversion(rec, cua_receipt, converter_sha256=None): +def inspect_conversion(rec, cua_receipt, converter_sha256=None, resolve_path=Path): """CUA receipt and converter pin are independent of conversion assertions.""" - raw, png = Path(rec['raw_path']), Path(rec['png_path']) + 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'] @@ -169,7 +169,7 @@ def inspect_conversion(rec, cua_receipt, converter_sha256=None): 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', str(raw), '--out', str(png)], + 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'): @@ -206,27 +206,92 @@ def affine_rect(matrix, rect): return lo + [max(p[i] for p in points)-lo[i] for i in (0, 1)] -def compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, local_to_screen, image, root=ROOT): - """All draw rows, full affine composition, and exact existing style colors. - Font advance is recorded by the pinned engine/font, not approximated using - a new text width threshold. No image similarity threshold is introduced. +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. """ - for point in ([0, 0], [1, 0], [0, 1]): - close(affine_point(viewport_to_screen, affine_point(local_to_viewport, point)), - affine_point(local_to_screen, point), 'viewport/screen composition') - rows = audit['rows'] + 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.""" expected = [] - tower, people, world = 'scripts/ui/pixel_tower_art.gd', 'scripts/ui/pixel_people.gd', 'scenes/main/world_preview.gd' + 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, at, text): - expected.append((category, identifier, at + [None, 14], world, text)) + def badge(category, identifier, b): + expected.append((category, identifier, b['at'] + [None, 14], 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['at'], b['text']) + 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']: @@ -236,36 +301,254 @@ def compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, loc if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']: actor(a, 'rest') for i, b in enumerate(report['badges']): - badge('goods' if b['kind'] == 'supply' else 'business', str(i), b['at'], b['text']) - require(len(rows) == len(expected), 'rectangle coverage') - width, height, pixels = image - checked = [] - for row, (category, identifier, rect, source, text) in zip(rows, expected): - require((row['category'], row['id'], row['style_source']) == (category, identifier, source), 'rectangle row identity') + 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 + + +def validate_drawing_rows(report, audit, root=ROOT): + 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 row['font_width'] > 0, 'badge font/text') + 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') rect[2] = row['font_width'] + 6 - close(row['rect'], rect, 'local drawing rectangle') - viewport = affine_rect(local_to_viewport, rect) - screen = affine_rect(viewport_to_screen, viewport) - close(row['viewport_rect'], viewport, 'viewport rectangle') - close(row['screen_rect'], screen, 'screen 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, 'drawing style palette') - x, y, w, h = screen - require(w > 0 and h > 0 and x >= 0 and y >= 0 and x+w <= width and y+h <= height, - 'drawing rectangle outside screenshot') - # Badge ink is distinct from its translucent backing. Actor ink/skin - # and facility colors are taken verbatim from their pinned draw source. - palette = {'fff2ce'} if text is not None else set(colors) - palette -= {'303c40', 'c6d3b6', 'd5dec4'} # shadow / sky decorations - rgb = {tuple(bytes.fromhex(c)) for c in palette} - found = any(tuple(pixels[py][px][:3]) in rgb - for py in range(math.ceil(y), math.floor(y+h)) - for px in range(math.ceil(x), math.floor(x+w))) - require(found, 'missing drawing pixels: ' + category + '/' + str(identifier)) - checked.append(category + '/' + str(identifier)) - return checked + 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): + """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 = [], [] + try: + validate_drawing_rows(report, audit, root) + 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'] == '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))) + + +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): + """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') + 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]) + frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT) + 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') + 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): @@ -1171,9 +1454,18 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): # Screenshot actions observe a stable report; input before/after # screenshots are provenance only and never relabelled as this frame. if request['action'] == 'screenshot': - checked = compare_rectangles(report, decode(request['rectangles']), request['local_to_viewport'], - request['viewport_to_screen'], request['local_to_screen'], image, root) - pixel_categories.update(c.split('/')[0] for c in checked) + 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) + 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')) 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 @@ -1301,9 +1593,15 @@ def main(argv=None): 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) args = parser.parse_args(argv) + 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: build = os.environ.get('BUILD_OUTPUT_DIR') if not build: diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 609eab9..b401740 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -288,27 +288,6 @@ class InfrastructureTests(unittest.TestCase): if change == 'id': q[0]['ack']['sequence'] = 2 with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members) - def test_rectangle_pixel_and_transform_negatives(self): - 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()))) - report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[], - actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)]) - row = 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) - audit = dict(rows=[row]) - matrix = [1, 0, 0, 1, 0, 0] - image = (24, 24, [[(113, 157, 148)]*24 for _ in range(24)]) - self.assertEqual(check.compare_rectangles(report, audit, matrix, matrix, matrix, image), ['staff/staff/s']) - for change in ('rectangle', 'pixels', 'transform', 'coverage', 'palette'): - with self.subTest(change=change): - a, m, img = copy.deepcopy(audit), matrix[:], image - if change == 'rectangle': a['rows'][0]['rect'][0] += 1 - if change == 'pixels': img = (24, 24, [[(0, 0, 0)]*24 for _ in range(24)]) - if change == 'transform': m[4] = 10 - if change == 'coverage': a['rows'].clear() - if change == 'palette': a['rows'][0]['style_colors'] = ['000000'] - with self.assertRaises(ValueError): check.compare_rectangles(report, a, matrix, m, matrix, img) - 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)))) @@ -568,5 +547,215 @@ class ComparisonTests(unittest.TestCase): 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)]) + 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 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)]) + 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_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()) + + if __name__ == '__main__': unittest.main() From 56bf5a3e865c528611f3f7b1d93b4a7b640f8ea8 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 10:53:39 +0900 Subject: [PATCH 10/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/run_p23_life_observation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py index 6592539..4eff71f 100644 --- a/tools/run_p23_life_observation.py +++ b/tools/run_p23_life_observation.py @@ -20,7 +20,7 @@ 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 = 900 +OS_CHILD_TIMEOUT_SECONDS = 3600 RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd' def collect_screenshots(out, pair, role): From 336a8cd0b30db1add11869ebceab847db693f531 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 10:57:46 +0900 Subject: [PATCH 11/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scenes/main/world_preview.gd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index 15b2366..0ba39c5 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -599,7 +599,7 @@ func _build_life_observation() -> Dictionary: model[&"guests"] = guests model[&"staff"] = staff model[&"tasks"] = tasks - var bridge: Variant = _service(&"supply_logistics_bridge") + 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] = [] From 49445d4a4d38281eb29dab1c1f85dd2b8ffbed11 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 12:08:24 +0900 Subject: [PATCH 12/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OPUS_R10_BUDGET_REPAIR_DIRECTION.json | 7 + .../p23-03/OPUS_R10_BUDGET_RUN_DIRECTION.json | 7 + .../p23-03/R10_BUDGET_REPAIR_SUBMISSION.md | 45 +++ .../execution/p23-03/R10_CONTEXT_GAP.md | 45 +++ .../p23-03/ROOT_OS_R10_BUDGET_FINDING.json | 335 ++++++++++++++++++ tests/fixtures/p23-03/observation.gd | 41 ++- tools/check_p23_life_observation_evidence.py | 30 ++ tools/test_p23_life_observation_evidence.py | 24 ++ 8 files changed, 528 insertions(+), 6 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_REPAIR_DIRECTION.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_RUN_DIRECTION.json create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/R10_BUDGET_REPAIR_SUBMISSION.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/R10_CONTEXT_GAP.md create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/ROOT_OS_R10_BUDGET_FINDING.json diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_REPAIR_DIRECTION.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_REPAIR_DIRECTION.json new file mode 100644 index 0000000..a10bc94 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_REPAIR_DIRECTION.json @@ -0,0 +1,7 @@ +{ + "action": "request-repair", + "task_id": "P23-03", + "reason": "Same-task repair for P23-03. Keep submitted 336a8cd and all earlier work, and keep every failure record (R10 B: 484 checks / 11 failures; validate 4958d8f55b6b458694bf158769ce3bff). Root cause, confirmed in the code: in tests/fixtures/p23-03/observation.gd, prepare() at lines 48-77 adds up only the registered construction_cost of the builds, including B's EV, to set a common diagnostic budget. Lines 93-99 then hire 5 candidates (staff_akane/gina/chika/iris/juno) and buy 40 RAW_FOOD without including either cost in that budget. Construction uses up all of B's cash, so all 5 hires fail with VALIDATION_FAILED (available 0) and the purchase fails with INSUFFICIENT_FUNDS. A succeeds. This is an omission in the fixture's budget, not a gap in production values.\n\nRepair, inside allowed paths only (tests/fixtures/p23-03/**, tools/check_p23_life_observation_evidence.py, tools/test_p23_life_observation_evidence.py, docs/plans/.../execution/p23-03/** metadata):\n(1) In prepare(), raise the common A/B diagnostic `needed` by:\n- the actual signing_cost of each of the 5 candidates, read at runtime from the registered candidate definitions (the catalog's signing_cost field, scripts/data/mvp_content_catalog.gd:323);\n- the gold cost of buying 40 RAW_FOOD, taken from the existing quote path (SupplyCatalog unit_price via PurchaseQuoteV2.make / the supply_management_service quote). Use effective_total if the actual quote applies a modifier.\nRead these values from the runtime registry/quote at the time they are needed. Do not hard-code numbers, add a margin, change default resources, or change product/runtime data. Record each cost source (definition/candidate/item id with its value), the totals, and the resulting deltas in the budget record. A and B must get the same deltas. The improvement EV cost stays in both budgets, as it is now.\n(2) Have the fixture check the receipt of each setup command (build, set_facility_conditions, hire, assignment, purchase, kitchen production) and expect() it to succeed, so a failed setup step shows up as a named failure and is not silently skipped.\n(3) Keep everything else the same: the common A/B inputs, the declared demand/arrival cohort, the native comparison's 48-demand, the 7 conditions and 6 categories, staff/goods coverage, normal OS plus distinct-PID load, full177, the independent Opus review, integration/final, 30/88 with no partial credit, the OS child timeout of 3600, worker/native 900 and the null cap.\n(4) Add a cheap local Python check, in the checker or its test, that statically or via the recorded budget confirms every planned spend (construction, signing, purchase) is counted in needed/deltas. Include a negative test for a missing spend category, so this mistake is caught before an expensive OS replay.\n(5) Update the p23-03 execution metadata to describe the finding and the repair honestly.\n\nThe worker runs only Python syntax checks and the checker's local tests, then makes a genuine submit. No Godot, native, OS or full regression runs, and Root evidence is expected to be missing at submission. Afterwards Root runs the exact final proof on the frozen source, reusing only evidence whose applicability matches. No new UI, product/runtime changes, broad hardening, billing or permission changes.", + "codex_model": "gpt-6.1-sol", + "codex_model_reason": "The repair is small and well understood: one fixture function needs signing and purchase costs added from existing catalog fields and the quote API, plus receipt assertions and a local Python preflight with a negative test. The root cause is already confirmed and nothing here needs new design or deep cross-system reasoning, so the default Sol is enough on quality and cheaper and faster than Astra." +} diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_RUN_DIRECTION.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_RUN_DIRECTION.json new file mode 100644 index 0000000..79d635e --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/OPUS_R10_BUDGET_RUN_DIRECTION.json @@ -0,0 +1,7 @@ +{ + "action": "run", + "task_id": "P23-03", + "reason": "The Runner shows P23-03 in repair_ready with 19 repairs, and the owner has dispatched the recorded R10 budget repair decision. run is a valid action and it is the next step, so this is not a repeat of a completed transition. The run keeps the R10 direction exactly as narrow as recorded and starts from the clean submitted base 336a8cd0b30db1add11869ebceab847db693f531. The worker stays inside the task contract's allowed_paths. It may only run local Python checks: syntax, the checker, and the checker's negative tests. It does not run Godot, native, OS, or full regression; Root runs those only after a genuine submission. The worker must say that Root evidence is missing at submission rather than mark anything PASS. Root's capacity recovery runs separately and must finish before proof replay; Root does not edit source. This run does not change the spec, the acceptance denominator, the budget or caps, or approval, and it does not touch main integration. Independent Opus review and Root validation are still required before acceptance.", + "codex_model": "gpt-6-astra", + "codex_model_reason": "I chose gpt-6-astra over the default because the difficulty justifies it. (1) The frozen spec's worker_instructions name one Codex Astra worker, and the recorded commander direction (run 83e836e2...) set codex_model gpt-6-astra. Changing the model during a narrow repair would break the recorded direction. (2) There have been 19 repair cycles, which shows the remaining defects are subtle consistency problems across the observation report, the strict checker that recomputes fields, the evidence manifests, and the same-demand layout A/B budget and seed setup. These need careful reasoning across files more than speed. (3) The worker only runs local Python, so per-run usage is bounded. A stronger model that gets it right in one pass should cost less overall than more failed cycles on the default model." +} 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/ROOT_OS_R10_BUDGET_FINDING.json b/docs/plans/original-completion-2026-10-02/execution/p23-03/ROOT_OS_R10_BUDGET_FINDING.json new file mode 100644 index 0000000..ed0e006 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/ROOT_OS_R10_BUDGET_FINDING.json @@ -0,0 +1,335 @@ +{ + "candidate": "336a8cd0b30db1add11869ebceab847db693f531", + "r10_path": "/Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p23-03-native-os-atomic-336a8cd-r10", + "terminal_error": "AssertionError: native OS collection failed: AssertionError: ordinary native OS exit0", + "cleanup_errors": [], + "A_writer_pid": 56892, + "A_reader_pid": 60176, + "B_writer_pid": 61335, + "B_reader": "not run; B writer failed", + "all_conditions_accepted": false, + "formal_credit": 0, + "confirmed_failure": "B construction-only diagnostic budget leaves zero cash; 5 hires and raw-food purchase rejected. Do not change production values or weaken conditions.", + "actual_command_objects": { + "A": [ + { + "object_sha256": "0d360907ad9289b7db5bc759a220cf274184f18cff3fe98c912941fed22fb1a9", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_akane" + }, + "result": { + "code": "OK", + "command_id": 24, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "candidate_id": "staff_akane", + "staff_id": "staff_akane", + "role_id": "receptionist", + "daily_wage": 120, + "employment_kind": "FREE_HIRE", + "signing_cost": 180, + "shift_id": "shift_00000001_d00000", + "player_cash": 1320 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "6dfd11522dee4b5ac9e1eefad4c2a27f01265cd46f1bb3e364665a85cd78769a", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_gina" + }, + "result": { + "code": "OK", + "command_id": 26, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "candidate_id": "staff_gina", + "staff_id": "staff_gina", + "role_id": "porter", + "daily_wage": 110, + "employment_kind": "FREE_HIRE", + "signing_cost": 150, + "shift_id": "shift_00000003_d00000", + "player_cash": 1170 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "e87798f0030e97a630c76ed3ad9ecdc4e032a090efc5fbb4a39ac606857828dd", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_chika" + }, + "result": { + "code": "OK", + "command_id": 28, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "candidate_id": "staff_chika", + "staff_id": "staff_chika", + "role_id": "cook", + "daily_wage": 145, + "employment_kind": "FREE_HIRE", + "signing_cost": 250, + "shift_id": "shift_00000005_d00000", + "player_cash": 920 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "00c167c28d271fbe133642650ab17b1240e73abd1d5fbb1bcea683f62e603c26", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_iris" + }, + "result": { + "code": "OK", + "command_id": 30, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "candidate_id": "staff_iris", + "staff_id": "staff_iris", + "role_id": "attendant", + "daily_wage": 130, + "employment_kind": "FREE_HIRE", + "signing_cost": 210, + "shift_id": "shift_00000007_d00000", + "player_cash": 710 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "969d0116e1bcc57e28019273e401ff1bbed54ca5ab22455d677889e2375f45f5", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_juno" + }, + "result": { + "code": "OK", + "command_id": 32, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "candidate_id": "staff_juno", + "staff_id": "staff_juno", + "role_id": "attendant", + "daily_wage": 140, + "employment_kind": "FREE_HIRE", + "signing_cost": 240, + "shift_id": "shift_00000009_d00000", + "player_cash": 470 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "092af99c4a2f84786641430e6c1cebce62f813b69dbac5d5ac397a3017fe6a84", + "decoded": { + "command": "purchase_supplies", + "payload": { + "warehouse_id": "facility_00000004", + "item_id": "ingredient/raw_food", + "quantity": 40 + }, + "result": { + "code": "OK", + "command_id": 34, + "target_id": "", + "message": "", + "resolution_hints": [], + "details": { + "ok": true, + "reason": "", + "total_cost": 120, + "cash_after": 350, + "quantity_after": 40, + "max_quantity": 100, + "warehouse_id": "facility_00000004", + "item_id": "ingredient/raw_food", + "quantity": 40, + "transaction_id": "ledger_00000060" + }, + "is_replay": false + }, + "minute": 910 + } + } + ], + "B": [ + { + "object_sha256": "bcb27db495ae9e94a20578e1aadcb64e0fa316bb1d4a6e954e581f4bdb34b296", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_akane" + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 25, + "target_id": "", + "message": "An account does not permit a negative balance.", + "resolution_hints": [], + "details": { + "account_id": "PLAYER_CASH", + "available": 0, + "resulting_balance": -180 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "81f9484c3fcce64e084a7e2ead8bf84dd8c56e144860624845eccb8c4218417a", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_gina" + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 27, + "target_id": "", + "message": "An account does not permit a negative balance.", + "resolution_hints": [], + "details": { + "account_id": "PLAYER_CASH", + "available": 0, + "resulting_balance": -150 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "16f4a094baa4204d4e254b6d52d824f32aa33e914e6bc1698aa933a8fa51207a", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_chika" + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 29, + "target_id": "", + "message": "An account does not permit a negative balance.", + "resolution_hints": [], + "details": { + "account_id": "PLAYER_CASH", + "available": 0, + "resulting_balance": -250 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "303e545649bf1c84c736293a6e1b54491befbc376b9a051d3f3c75659e2b147e", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_iris" + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 31, + "target_id": "", + "message": "An account does not permit a negative balance.", + "resolution_hints": [], + "details": { + "account_id": "PLAYER_CASH", + "available": 0, + "resulting_balance": -210 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "de1717d64250c9ae721fc38230d185b0621a066e01890f9fb4020b4c7e36e7c0", + "decoded": { + "command": "hire_staff_candidate", + "payload": { + "candidate_id": "staff_juno" + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 33, + "target_id": "", + "message": "An account does not permit a negative balance.", + "resolution_hints": [], + "details": { + "account_id": "PLAYER_CASH", + "available": 0, + "resulting_balance": -240 + }, + "is_replay": false + }, + "minute": 910 + } + }, + { + "object_sha256": "c257c14418959e558b88d4f47591c14726fff4763a30177275d8725cb6639b9f", + "decoded": { + "command": "purchase_supplies", + "payload": { + "warehouse_id": "facility_00000004", + "item_id": "ingredient/raw_food", + "quantity": 40 + }, + "result": { + "code": "VALIDATION_FAILED", + "command_id": 35, + "target_id": "", + "message": "Supply command rejected: INSUFFICIENT_FUNDS", + "resolution_hints": [], + "details": { + "ok": false, + "reason": "INSUFFICIENT_FUNDS", + "total_cost": 120, + "cash_after": -120, + "quantity_after": 40, + "max_quantity": 0 + }, + "is_replay": false + }, + "minute": 910 + } + } + ] + }, + "next": "Actual Opus diagnosis and narrow same-task repair before full regression" +} diff --git a/tests/fixtures/p23-03/observation.gd b/tests/fixtures/p23-03/observation.gd index 8b9a2ed..e65bfac 100644 --- a/tests/fixtures/p23-03/observation.gd +++ b/tests/fixtures/p23-03/observation.gd @@ -2,6 +2,7 @@ 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 = {} @@ -28,7 +29,7 @@ func setup() -> void: func command(id: StringName, payload: Dictionary) -> bool: var result: CommandResult = runtime.submit_command(id, payload) - expect(result.is_success(), String(id) + ": " + str(result.code)) + 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() @@ -46,7 +47,7 @@ func observe_progress() -> void: pass func prepare() -> void: - # Both layouts receive the same definition-derived diagnostic construction budget. + # 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)], @@ -59,6 +60,9 @@ func prepare() -> void: 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: @@ -66,13 +70,36 @@ func prepare() -> void: 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} + "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"]]) @@ -90,13 +117,15 @@ func prepare() -> void: 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 [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"], - [&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]: + 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": 40}) + &"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 diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 2f4a0ac..108419b 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -71,6 +71,35 @@ def require(value, message): 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() @@ -1597,6 +1626,7 @@ def main(argv=None): parser.add_argument('--layout-a', type=Path) parser.add_argument('--layout-b', 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') diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index b401740..3190465 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -18,6 +18,30 @@ sys.dont_write_bytecode = True import check_p23_life_observation_evidence as check +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()) From 19fc3f50c7ef66223a86b438d4327c5b1b308ed1 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 18:01:00 +0900 Subject: [PATCH 13/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution/p23-03/R23_REPAIR_NOTES.md | 443 +++++++++++ scenes/main/world_preview.gd | 276 ++++++- tests/fixtures/p23-03/passive_observation.gd | 3 + tests/fixtures/p23-03/r13_actor_overlap.json | 15 + .../p23-03/r13_service_region_gap.json | 17 + tests/ui/test_p23_life_observation.gd | 59 ++ tools/check_p23_life_observation_evidence.py | 416 ++++++++-- tools/test_p23_life_observation_evidence.py | 730 +++++++++++++++++- 8 files changed, 1888 insertions(+), 71 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/R23_REPAIR_NOTES.md create mode 100644 tests/fixtures/p23-03/r13_actor_overlap.json create mode 100644 tests/fixtures/p23-03/r13_service_region_gap.json 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/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index 0ba39c5..f1f78a4 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -79,6 +79,8 @@ 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 @@ -104,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 @@ -513,6 +519,8 @@ 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() @@ -524,6 +532,9 @@ func _draw_contents() -> void: 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"] @@ -540,11 +551,9 @@ func _draw_contents() -> void: _draw_legacy_stairs() _draw_elevators() _draw_other_layer() - _draw_congestion_overlay(bounds) _draw_life_badges() _draw_construction_labels() _draw_facility_labels() - _draw_actors() _draw_excavation_jobs() _draw_build_preview() _draw_excavation_preview() @@ -599,6 +608,7 @@ func _build_life_observation() -> Dictionary: 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 @@ -638,7 +648,14 @@ func _build_life_observation() -> Dictionary: var cell := Vector3i(x, y, layer_mode) var row: Dictionary = path.get_cell_traffic(cell) if not row.is_empty(): - traffic.append({&"cell": cell, &"traffic": row, &"rect": get_cell_rect(cell).grow(-2.0)}) + 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}) model[&"traffic"] = traffic return model @@ -758,9 +775,27 @@ 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: +func _draw_life_overlay() -> void: + if _life_observation.is_empty(): + return + # 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(&"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 rect: Rect2 = row[&"rect"] + var band: Rect2 = row[&"band_rect"] var traffic: Dictionary = row[&"traffic"] var density := int(traffic[&"current_density"]) var blocked := bool(traffic[&"temporary_block"]) @@ -768,13 +803,11 @@ func _draw_congestion_overlay(_bounds: Dictionary) -> void: 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") - var center := rect.get_center() - 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 if show_grid else 0.28), clampf(rect.size.y * 0.16, 2.0, 5.0)) + canvas.draw_rect(band, Color(color, 0.90)) if blocked: - draw_line(rect.position, rect.end, Color(color, 0.90 if show_grid else 0.28), 2.0) - draw_line(Vector2(rect.end.x, rect.position.y), Vector2(rect.position.x, rect.end.y), Color(color, 0.90 if show_grid else 0.28), 2.0) + var cross := band.grow(-1.0) + canvas.draw_line(cross.position, cross.end, Color(color, 0.90), 2.0) + canvas.draw_line(Vector2(cross.end.x, cross.position.y), Vector2(cross.position.x, cross.end.y), Color(color, 0.90), 2.0) ## Visual-only level comes from the existing live FacilityInstance, not the @@ -887,13 +920,15 @@ func _draw_world_entity_icons() -> void: draw_rect(rect.grow(-p),Color("f5d582"),false,2*p) -func _draw_readable_badge(at: Vector2, text: String) -> void: +func _draw_readable_badge(at: Vector2, text: String, canvas: CanvasItem = null) -> void: + if canvas == null: + canvas = self var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6 var rect := Rect2(at, Vector2(width, 14)) 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", 0.94)) + canvas.draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce")) func _prepare_room_presentations() -> void: @@ -1146,8 +1181,6 @@ func _draw_facility_labels() -> void: func _draw_elevators() -> void: for row: Dictionary in _life_observation.get(&"cabins", []): PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"]) - for actor: Dictionary in row[&"passengers"]: - _draw_observed_actor(actor) PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"]) _draw_readable_badge(row[&"badge_at"], row[&"badge"]) for badge: Dictionary in _life_observation.get(&"landing_badges", []): @@ -1188,8 +1221,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]: var doors_open := operational and phase in [&"IDLE", &"BOARDING", &"ALIGHTING"] 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 @@ -1200,7 +1232,7 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]: 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) 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, @@ -1240,9 +1272,9 @@ func _tracked_elevator_floor(shaft_id: StringName, current_floor: float, observa 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) + _draw_observed_actor(row, canvas) func _observe_actors(observation: Dictionary) -> Array[Dictionary]: @@ -1291,6 +1323,9 @@ func _observe_actors(observation: Dictionary) -> Array[Dictionary]: continue 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: @@ -1412,11 +1447,13 @@ func _draw_pixel_actor(center: Vector2, key: String, staff: bool, state: int, re _draw_observed_actor(_actor_drawing(center, key, staff, state, receiving, carrying, activity, riding)) -func _draw_observed_actor(row: Dictionary) -> void: +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"] - draw_rect(Rect2(at + Vector2(5, 17) * p, Vector2(8, 1) * p), Color("303c40", 0.2)) - People.draw(self, at, p, row[&"mode"], row[&"frame"], row[&"variant"], row[&"staff"], row[&"flip"]) + 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: @@ -1454,6 +1491,197 @@ func _actor_drawing(center: Vector2, key: String, staff: bool, state: int, recei &"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} + + +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_track_region("staff/"+String(carrier[&"id"]), carrier[&"cell"], model) + var rect: Rect2 = result[&"bounds"] + rect.position.y -= 12*p + result[&"bounds"] = rect + 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} + 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 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} + region[&"row"] = row + if not region[&"transit"]: + members.append(region) + var groups := _life_fitted_groups(members) + model[&"actor_aggregations"] = [] + var hidden: Dictionary = {} + 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: var excavation: Variant = _service(&"excavation_service") if excavation == null or not excavation.has_method(&"get_jobs"): diff --git a/tests/fixtures/p23-03/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd index 8cc62ec..e33004f 100644 --- a/tests/fixtures/p23-03/passive_observation.gd +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -166,6 +166,9 @@ func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary: 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"])) 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/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index fb9f077..2e83427 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -56,6 +56,61 @@ func run() -> void: 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() @@ -67,6 +122,10 @@ func run() -> void: 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", &"actors"], "traffic drawn before 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"): diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 108419b..8dc8d1a 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -329,6 +329,8 @@ def drawing_rectangle_rows(report): 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', []): @@ -337,7 +339,77 @@ def drawing_rectangle_rows(report): return expected -def validate_drawing_rows(report, audit, root=ROOT): +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): @@ -367,15 +439,17 @@ def encode_png(rows): + chunk(b'IDAT', zlib.compress(raw)) + chunk(b'IEND', b'')) -def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT): +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) + 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) @@ -415,6 +489,9 @@ def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT): 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')} @@ -448,7 +525,8 @@ def frame_rows(report, audit, request, receipt, images, clicks, root=ROOT): 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))) + expected_row_count=len(drawing_rectangle_rows(report)), + transit_authority=transit_authority) def write_frame_review(directory, frame, provenance): @@ -551,7 +629,14 @@ def inspect_offline_mirror(directory): for original in (str(Path(index['source'])/conversion_path.relative_to(directory)), conversion['png_path']): inspected.append(by_original[original]) - frame = frame_rows(report, audit, request, receipt, images, clicks, ROOT) + 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') @@ -720,9 +805,56 @@ def compare_traffic(snapshot, report): 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=[x + 2, y + 2, w - 4, h - 4])) + rect=rect, band_rect=band)) close(report['traffic'], rows, 'traffic and cell rect') + validate_life_overlay(report) + + +def validate_life_overlay(report, root=ROOT): + """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', '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_observed_actor(actor, _life_overlay)') + < overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order') + actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']] + 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') + if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']: + continue + for actor in actors: + core = actor_identity_core(actor) + intersects = (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 intersects, 'traffic band covers actor core: ' + actor['key']) def compare_queues(snapshot, report): @@ -935,27 +1067,12 @@ def compare_rest(state, report, audit, root=ROOT): if selected.get('resting'): resting[sid] = selected['facility_id'] close(report['resting_staff'], resting, 'draw resting_staff') - transform = Transform(runtime, report) - drawings = {r['key']: r for r in report['actor_drawings']} - for sid, fid in resting.items(): - member = next(m for m in members if m['staff_id'] == sid) - if member['current_cell'][2] != report['layer'] or sid in report['carried_patients']: - continue - room = report['rooms'].get(fid, {}) - cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), []) - if not cells: - continue # renderer uses its ordinary tracked fallback - require(sid in room.get('people', []), 'rest room occupant missing') - slot = room['people'].index(sid) - anchors = room_slots(transform.room(cells), transform.dim[0] / 16) - key = 'staff/' + sid - if key in report['boarded'] or key in report['queue_hidden']: - require(key not in drawings, 'hidden resting drawing') - elif slot >= len(anchors): - require(key not in drawings, 'rest slot overflow drawing') - else: - close(drawings[key]['center'], [anchors[slot][0], anchors[slot][1] - transform.dim[1] * .5], - 'resting staff position') + # 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 @@ -1129,8 +1246,105 @@ def compare_motion(state, report, audit): 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 compare_actor_geometry(state, 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') @@ -1139,7 +1353,7 @@ def compare_actor_geometry(state, report): counts, expected = {}, [] def track_position(key, cell): track = report['actor_tracks'][key] - close(track['cell'], cell, 'actor authoritative cell') + 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)] @@ -1182,7 +1396,10 @@ def compare_actor_geometry(state, report): continue center = track_position(key, cell) special = True - if sid in report['carried_patients']: + 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']) @@ -1206,31 +1423,127 @@ def compare_actor_geometry(state, report): 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)) - require([r['key'] for r in report['actor_drawings']] == [r[0] for r in expected], 'actor drawing coverage/order') - pairs = list(zip(report['actor_drawings'], expected)) + 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'][:columns*2] - require(len(cabin['passengers']) == len(passengers), 'passenger coverage') - for i, (actor_id, row) in enumerate(zip(passengers, cabin['passengers'])): + 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)*9)*p+3*p-height*.5] + 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 - pairs.append((row, (key, center, is_staff, False, carried, True))) - for row, (key, center, is_staff, receiving, carrying, riding) in pairs: - close(row['center'], center, 'actor center/interpolation') + expected.append((key, center, is_staff, False, carried, True)) + cabin_bounds[key] = cabin['rect'] + people = {'guest/'+r['guest_id']: r for r in runtime['guests']['guests']} + people.update({'staff/'+sid: r for sid, r in staff.items()}) + def tracked_bounds(key, cell): + track = report['actor_tracks'][key] + transit = actor_track_transit(report, key, cell) + rect = transform.rect(cell) + if transit: + other = transform.rect(track['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] + return rect, transit + members = [] + for key, center, is_staff, receiving, carrying, riding in expected: + person = people[key] + cell = person['current_cell'] + track = report['actor_tracks'].get(key, {}) + region, transit = (cabin_bounds[key], False) if riding else tracked_bounds(key, cell) + fid = '' + if not riding: + if key in report['queue_positions']: + cx, cy = report['queue_positions'][key] + region, transit = [cx-transform.dim[0]/2, cy-height/2, transform.dim[0], height], False + elif not is_staff: + target = person['target_facility_id'] + if receiving and target in facilities and key.split('/', 1)[1] in report['rooms'].get(target, {}).get('people', []): + fid = target + else: + sid = person['staff_id'] + if sid in report['carried_patients']: + carrier = staff[report['carried_patients'][sid]] + region, transit = tracked_bounds('staff/'+carrier['staff_id'], carrier['current_cell']) + region[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 target in facilities 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: + region, transit = [rect[0]+slot*rect[2]/columns, rect[1], rect[2]/columns, rect[3]], False + if fid and occupancy.get(fid): + region, transit = transform.room(occupancy[fid]), False scale = .5 if riding else .75 scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale - close(row['scale'], scale, 'actor 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] - close(row['at'], rounded, 'actor drawing position') - require((row['key'], row['staff'], row['receiving'], row['carrying'], row['riding']) == - (key, is_staff, receiving, carrying, riding), 'actor drawing attributes') + 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) + 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) + 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: + 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'] @@ -1270,6 +1583,20 @@ def compare_layouts(a, b): 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') @@ -1458,7 +1785,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): 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] - close(read_object(stream, capture['report']), report, 'request draw report binding') + 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') @@ -1485,7 +1812,8 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): 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) + 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'], diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 3190465..f303854 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -18,6 +18,165 @@ sys.dont_write_bytecode = True import check_p23_life_observation_evidence as check +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 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, 'traffic band covers actor core'): + 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 SetupBudgetTests(unittest.TestCase): def setUp(self): self.source = (check.ROOT / 'tests/fixtures/p23-03/observation.gd').read_text() @@ -452,6 +611,83 @@ class SemanticTests(unittest.TestCase): 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), @@ -464,11 +700,178 @@ class SpatialTests(unittest.TestCase): 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=[])) + goods=dict(requests=[], deliveries=[]), actor_drawings=[], cabins=[], + observation_serial=1, overlay_consumed_serial=1, + overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2]) + + 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) @@ -537,6 +940,247 @@ class SpatialTests(unittest.TestCase): 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] + 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') + 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', [0,0,0]) + self.drawing('guest/floor', [108,214]) + cabin['passengers'][0]['at'][0] = 108 + 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 + 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 ComparisonTests(unittest.TestCase): def pair(self): declaration = dict(metric='sum_destination_arrival_minutes_entire_cohort', @@ -647,7 +1291,8 @@ class OfflineGeometryTests(unittest.TestCase): 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)]) + 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], @@ -664,6 +1309,79 @@ class OfflineGeometryTests(unittest.TestCase): 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], @@ -671,7 +1389,8 @@ class FrameReviewTests(unittest.TestCase): 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)]) + 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()))) @@ -684,6 +1403,11 @@ class FrameReviewTests(unittest.TestCase): 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() From 9a26f9ab8c6691ba3cbe32ac4bc4525f4a9c7106 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 18:30:42 +0900 Subject: [PATCH 14/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p23-03/REPAIR28_TRAFFIC_PROJECTION.md | 26 + .../p23-03/repair28_frame180_projection.json | 2011 +++++++++++++++++ tools/check_p23_life_observation_evidence.py | 38 +- tools/test_p23_life_observation_evidence.py | 101 +- 4 files changed, 2172 insertions(+), 4 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_TRAFFIC_PROJECTION.md create mode 100644 tests/fixtures/p23-03/repair28_frame180_projection.json 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/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/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 8dc8d1a..3501717 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -812,10 +812,10 @@ def compare_traffic(snapshot, report): rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'}, rect=rect, band_rect=band)) close(report['traffic'], rows, 'traffic and cell rect') - validate_life_overlay(report) + validate_life_overlay(report, snapshot=snapshot) -def validate_life_overlay(report, root=ROOT): +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 @@ -842,6 +842,25 @@ def validate_life_overlay(report, root=ROOT): < overlay.find('_draw_observed_actor(actor, _life_overlay)') < overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order') 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) for row in report.get('traffic', []): x, y, w, h = row['rect'] thickness = max(2, min(5, h * .16)) @@ -850,11 +869,24 @@ def validate_life_overlay(report, root=ROOT): close(row['band_rect'], expected, 'traffic band rectangle') if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']: continue + intervals = [] for actor in actors: core = actor_identity_core(actor) intersects = (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 intersects, 'traffic band covers actor core: ' + actor['key']) + require(not intersects 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']) + if intersects: + 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): diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index f303854..7bb3d24 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -157,7 +157,7 @@ class LifeOverlayTests(unittest.TestCase): def test_core_crossed_by_actual_band_fails(self): report = self.report() report['actor_drawings'][0]['at'][1] = 60 - with self.assertRaisesRegex(ValueError, 'traffic band covers actor core'): + with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band'): check.validate_life_overlay(report) def test_forged_band_geometry_fails(self): @@ -177,6 +177,105 @@ class LifeOverlayTests(unittest.TestCase): 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'] + + 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, 'floor actor core over traffic band'): + 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, 'floor actor core over traffic band: 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))] + 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() From 6c13940cf4321548491495fded26b82645aa58da Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 14:17:14 +0900 Subject: [PATCH 15/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../p23-03/REPAIR28_CONTINUATION_HANDBACK.md | 86 ++++++ scenes/main/world_preview.gd | 87 ++++-- scripts/ui/room_presentation.gd | 6 +- tests/fixtures/p23-03/passive_observation.gd | 2 +- .../p23-03/repair28_r15_negative.json | 14 + tests/ui/test_p23_life_observation.gd | 71 ++++- tools/check_p23_life_observation_evidence.py | 241 ++++++++++++++++- tools/run_p23_life_observation.py | 10 +- tools/test_p23_life_observation_evidence.py | 255 +++++++++++++++++- 10 files changed, 724 insertions(+), 50 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR28_CONTINUATION_HANDBACK.md create mode 100644 tests/fixtures/p23-03/repair28_r15_negative.json 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 82e2931..08749b3 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 @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1", + "source_sha256_at_schema_audit": "610ff2f33ec0e42bdca55d692e640c9bd8fe9244b9b49b0263419b1998ff9e9e", "authority": { "path": "docs/spec/acceptance/P23-03.json", "scope": "component and read-only observation only; native unexecuted; whole unit blocked" 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/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index f1f78a4..219834a 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -549,9 +549,7 @@ func _draw_contents() -> void: _draw_world_cells(bounds, map_rect) _draw_world_entity_icons() _draw_legacy_stairs() - _draw_elevators() _draw_other_layer() - _draw_life_badges() _draw_construction_labels() _draw_facility_labels() _draw_excavation_jobs() @@ -639,6 +637,14 @@ func _build_life_observation() -> Dictionary: &"at": rect.position + Vector2(2, 16), &"requests": requests, &"text": RoomPresentation.supply_label(requests)}) model[&"badges"] = badges + 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() @@ -655,14 +661,39 @@ func _build_life_observation() -> Dictionary: 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}) + traffic.append({&"cell": cell, &"traffic": row, &"rect": rect, &"band_rect": band, + &"segment_rects": _traffic_segments(band, rect, obstacles)}) model[&"traffic"] = traffic return model -func _draw_life_badges() -> void: +## 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 _draw_life_badges(canvas: CanvasItem) -> void: for badge: Dictionary in _life_observation.get(&"badges", []): - _draw_readable_badge(badge[&"at"], badge[&"text"]) + _draw_readable_badge(badge[&"at"], badge[&"text"], canvas) func _draw_background() -> void: @@ -783,6 +814,9 @@ func _draw_life_overlay() -> void: _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"]: @@ -795,7 +829,6 @@ func _draw_life_overlay() -> void: func _draw_congestion_overlay(canvas: CanvasItem) -> void: for row: Dictionary in _life_observation.get(&"traffic", []): - var band: Rect2 = row[&"band_rect"] var traffic: Dictionary = row[&"traffic"] var density := int(traffic[&"current_density"]) var blocked := bool(traffic[&"temporary_block"]) @@ -803,11 +836,13 @@ func _draw_congestion_overlay(canvas: CanvasItem) -> void: 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") - canvas.draw_rect(band, Color(color, 0.90)) - if blocked: - var cross := band.grow(-1.0) - canvas.draw_line(cross.position, cross.end, Color(color, 0.90), 2.0) - canvas.draw_line(Vector2(cross.end.x, cross.position.y), Vector2(cross.position.x, cross.end.y), Color(color, 0.90), 2.0) + # 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 @@ -920,14 +955,19 @@ func _draw_world_entity_icons() -> void: draw_rect(rect.grow(-p),Color("f5d582"),false,2*p) +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 width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x+6 - var rect := Rect2(at, Vector2(width, 14)) + var rect := _readable_badge_rect(at, text) if not rect.intersects(Rect2(Vector2.ZERO, size)): return - canvas.draw_rect(rect, Color("34413b", 0.94)) + canvas.draw_rect(rect, Color("34413b")) canvas.draw_string(ThemeDB.fallback_font, at+Vector2(3, 11), text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10, Color("fff2ce")) @@ -1017,7 +1057,8 @@ func _observe_queues() -> Dictionary: 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)) @@ -1032,6 +1073,8 @@ func _observe_queues() -> Dictionary: 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"]: @@ -1051,7 +1094,7 @@ func _observe_queues() -> Dictionary: 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} + 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: @@ -1178,13 +1221,13 @@ 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: +func _draw_elevators(canvas: CanvasItem) -> void: for row: Dictionary in _life_observation.get(&"cabins", []): - PixelArt.elevator_cabin(self, row[&"rect"], row[&"pixel"], layer_mode == LayerMode.BACK, row[&"operational"]) - PixelArt.elevator_gate(self, row[&"rect"], row[&"pixel"], row[&"doors_open"]) - _draw_readable_badge(row[&"badge_at"], row[&"badge"]) + 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"]) + _draw_readable_badge(badge[&"at"], badge[&"text"], canvas) func _observe_elevators(observation: Dictionary) -> Array[Dictionary]: @@ -1237,8 +1280,10 @@ func _observe_elevators(observation: Dictionary) -> Array[Dictionary]: 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(): diff --git a/scripts/ui/room_presentation.gd b/scripts/ui/room_presentation.gd index 20dd175..0c1fa7f 100644 --- a/scripts/ui/room_presentation.gd +++ b/scripts/ui/room_presentation.gd @@ -82,7 +82,11 @@ static func elevator_label(shaft: Dictionary) -> String: value = "満員 " + value if not bool(shaft[&"operational"]): value = "停止 " + value - return 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: diff --git a/tests/fixtures/p23-03/passive_observation.gd b/tests/fixtures/p23-03/passive_observation.gd index e33004f..4bb3cf8 100644 --- a/tests/fixtures/p23-03/passive_observation.gd +++ b/tests/fixtures/p23-03/passive_observation.gd @@ -193,7 +193,7 @@ func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary: 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(width, 14)), + 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: 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 index 2e83427..9410247 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -5,6 +5,74 @@ const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd") var checks := 0 var failures := 0 +class QueueWorld extends RefCounted: + 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.x >= -1 and cell.x <= 2 + func get_occupant(_cell: Vector3i) -> StringName: + return &"" + +class QueueLift extends RefCounted: + var layer := 0 + 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": 0.3, + &"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 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") @@ -25,6 +93,7 @@ func run() -> void: return var clock: Variant = root.get_node("GameClock") clock.set_paused(true) + repair28_geometry() # Isolated real typed components test state labels, without claiming gameplay. var labels: Dictionary = {} for state: int in FacilityInstance.State.values(): @@ -123,7 +192,7 @@ func run() -> void: 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", &"actors"], "traffic drawn before actors") + 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. diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 3501717..8857f67 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -15,6 +15,7 @@ from pathlib import Path import re import sys import struct +import subprocess import zlib sys.dont_write_bytecode = True @@ -315,7 +316,7 @@ def drawing_rectangle_rows(report): 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, b['at'] + [None, 14], world, b['text'])) + 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)) @@ -419,7 +420,7 @@ def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None) 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') - rect[2] = row['font_width'] + 6 + 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') @@ -796,6 +797,56 @@ class Transform: 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 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) @@ -810,7 +861,8 @@ def compare_traffic(snapshot, report): 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)) + 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) @@ -821,7 +873,7 @@ def validate_life_overlay(report, root=ROOT, snapshot=None): 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', 'actors'], 'LifeOverlay pass order') + 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') @@ -839,8 +891,18 @@ def validate_life_overlay(report, root=ROOT, snapshot=None): 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() @@ -861,23 +923,34 @@ def validate_life_overlay(report, root=ROOT, snapshot=None): 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) - intersects = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2] + 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 intersects or id(actor) in cabin_actors + 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']) - if intersects: + # 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] @@ -915,7 +988,11 @@ def compare_queues(snapshot, report): for trip in elevators['trips']: if trip['state'] == 'QUEUED': add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from']) - lift_cells = {tuple(cell) for shaft in elevators['shafts'] for cell in shaft['stops']} + 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(), {}, {}, [] @@ -926,7 +1003,7 @@ def compare_queues(snapshot, report): for distance in range(1, 9): target = (cell[0] + direction * distance, cell[1], cell[2]) facility = facilities.get(owners.get(target)) - if target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')): + 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 @@ -1207,13 +1284,19 @@ def compare_cabin_geometry(snapshot, report): 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'], - doors_open=shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING'), + 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): @@ -1693,6 +1776,126 @@ def BLOCKERS(executed=None): 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 + + +def verify_composition(root, roles, source_map, inputs, read_git=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') + require(git('rev-parse', 'HEAD').decode().strip() == q, 'checkout is not Q') + 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() + 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)) + source_names = {p for p in tested if p.split('/', 1)[0] in + ('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools') + and Path(p).name != '.DS_Store' and '__pycache__' not in Path(p).parts} + source_names.update(('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md')) + require(set(source_map) == source_names, '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') + for path in sorted(required): + entry = tested.get(path) + require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path) + content = git('show', t + ':' + path) + 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} def normative_plan_text(root): @@ -1780,6 +1983,11 @@ def inspect_resume(writer, reader, saved, requests, formal_members): 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): @@ -1801,6 +2009,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): 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) @@ -1887,7 +2096,7 @@ def inspect_native_phases(directory, index, root=ROOT): and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker') -def check(root, directory): +def check(root, directory, root_roles=None): """Never infer completion of unimplemented gates from an index assertion.""" deficits = [] executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS} @@ -1897,8 +2106,13 @@ def check(root, directory): 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(root, root_roles, index['source_map'], input_map(root)) + 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: @@ -1985,6 +2199,8 @@ def main(argv=None): 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, + default=os.environ.get('P23_ROOT_COMPOSITION')) args = parser.parse_args(argv) check_setup_budget_source((args.project_root / 'tests/fixtures/p23-03/observation.gd').read_text()) if args.offline_mirror: @@ -1997,7 +2213,8 @@ def main(argv=None): if not build: print('BUILD_OUTPUT_DIR is required', file=sys.stderr) return 2 - deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence') + 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) 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 diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py index 4eff71f..3e3364f 100644 --- a/tools/run_p23_life_observation.py +++ b/tools/run_p23_life_observation.py @@ -235,12 +235,15 @@ def collect(args): 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 final submitted commit required') + 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) @@ -328,7 +331,8 @@ def collect(args): 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-v1', 'commit':commit, + 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), @@ -345,6 +349,8 @@ def main(argv=None): 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) diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 7bb3d24..067e394 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -18,6 +18,233 @@ 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 verify(self): + with mock.patch.object(Path, 'read_bytes', lambda p:self.files[self.t][str(p.relative_to(check.ROOT))]): + return check.verify_composition(check.ROOT,self.roles,self.source,self.inputs,self.git) + + 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 ActorNonoverlapTests(unittest.TestCase): def report(self, second_at): return dict(actor_drawings=[ @@ -123,12 +350,12 @@ class ActorNonoverlapTests(unittest.TestCase): class LifeOverlayTests(unittest.TestCase): def report(self): - return dict(observation_serial=7, overlay_consumed_serial=7, + 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))]) + traffic=dict(current_density=1, temporary_block=False))])) def test_front_and_back_valid_consumed_band(self): for layer in (0, 1): @@ -181,6 +408,7 @@ 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']: @@ -228,7 +456,7 @@ class ProjectionBandTests(unittest.TestCase): 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, 'floor actor core over traffic band'): + 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): @@ -247,7 +475,7 @@ class ProjectionBandTests(unittest.TestCase): 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, 'floor actor core over traffic band: staff/spoof'): + 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): @@ -266,6 +494,7 @@ class ProjectionBandTests(unittest.TestCase): # 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) @@ -807,6 +1036,7 @@ class SpatialTests(unittest.TestCase): 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]]) @@ -988,10 +1218,10 @@ class SpatialTests(unittest.TestCase): 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(queue_positions={'guest/g0': [100, 82]}, - queue_hidden={'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54], - text='待ち3人(他2人)', cell=[0, 0, 0], - members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=1, overflow=2)]) + 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): @@ -1031,6 +1261,7 @@ class SpatialTests(unittest.TestCase): 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) @@ -1169,6 +1400,7 @@ class AnchoredGeometryTests(unittest.TestCase): 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]) @@ -1233,6 +1465,7 @@ class AnchoredGeometryTests(unittest.TestCase): 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']) @@ -1241,9 +1474,8 @@ class AnchoredGeometryTests(unittest.TestCase): def test_passenger_and_floor_actor_front_and_back(self): cabin = self.cabin(1) - self.person('guest/floor', [0,0,0]) - self.drawing('guest/floor', [108,214]) - cabin['passengers'][0]['at'][0] = 108 + 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 @@ -1251,6 +1483,7 @@ class AnchoredGeometryTests(unittest.TestCase): 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) From 447f77a13b3aa56f44438ff30bef9c7f77979c62 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 14:24:10 +0900 Subject: [PATCH 16/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/check_p23_life_observation_evidence.py | 91 ++++++++-- tools/test_p23_life_observation_evidence.py | 172 ++++++++++++++++++- 2 files changed, 247 insertions(+), 16 deletions(-) diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 8857f67..3ac3c6f 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -5,6 +5,8 @@ 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 @@ -1848,7 +1850,11 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): 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') - require(git('rev-parse', 'HEAD').decode().strip() == q, 'checkout is not Q') + 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') @@ -1875,11 +1881,10 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): def document(revision): return strict_json(git('show', revision + ':' + CONTRACT)) verify_marker_join(document(BASE_COMMIT), document(s), document(m), document(t)) - source_names = {p for p in tested if p.split('/', 1)[0] in - ('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools') - and Path(p).name != '.DS_Store' and '__pycache__' not in Path(p).parts} - source_names.update(('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md')) - require(set(source_map) == source_names, 'incomplete tested-tree source/settings/QA map') + runner = module('p23_composition_source_filter', ROOT/'tools/run_all_tests.py') + with tested_checkout(root, roles, git) as view: + require(source_map == {p: row['sha256'] for p, row in runner.source_refs(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'} @@ -1889,7 +1894,8 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): entry = tested.get(path) require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path) content = git('show', t + ':' + path) - require((root/path).read_bytes() == content, 'checkout bytes differ from T: ' + 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) @@ -1898,6 +1904,41 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): return {k: roles[k] for k in names} +@contextlib.contextmanager +def tested_checkout(root, roles, read_git=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) + 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') + target = view/path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(git('show', tree + ':' + path.as_posix())) + yield view + + +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() @@ -2097,6 +2138,19 @@ def inspect_native_phases(directory, index, root=ROOT): 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} @@ -2110,7 +2164,10 @@ def check(root, directory, root_roles=None): '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(root, root_roles, index['source_map'], input_map(root)) + roles = verify_composition(checkout, root_roles, index['source_map'], input_map(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): @@ -2127,7 +2184,10 @@ def check(root, directory, root_roles=None): 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) - inspect_native_phases(directory, index, root) + 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) @@ -2199,8 +2259,7 @@ def main(argv=None): 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, - default=os.environ.get('P23_ROOT_COMPOSITION')) + 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: @@ -2208,13 +2267,17 @@ def main(argv=None): '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: + 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 - 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) + 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 diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 067e394..c8c5c4d 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -11,6 +11,8 @@ import hashlib import struct import zlib import tempfile +import os +import subprocess from pathlib import Path from unittest import mock @@ -182,7 +184,12 @@ class Repair28CompositionTests(unittest.TestCase): raise AssertionError(args) def verify(self): - with mock.patch.object(Path, 'read_bytes', lambda p:self.files[self.t][str(p.relative_to(check.ROOT))]): + 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) def test_valid_route_b_keeps_unchanged_allowed_path_from_main(self): @@ -245,6 +252,167 @@ class Repair28CompositionTests(unittest.TestCase): self.roles, self.source = original_roles, original_source +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') + 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_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=[ @@ -600,7 +768,7 @@ class SnapshotCellTests(unittest.TestCase): 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(dir=check.ROOT/'tests/fixtures/p23-03') as temporary: + with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) pair = root/'os-pair/A' request_dir = pair/'writer-request-0001' From 7f1920b329c71f1a86738beedd02caac686f2be5 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 14:41:54 +0900 Subject: [PATCH 17/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/check_p23_life_observation_evidence.py | 112 ++++++++++-- tools/test_p23_life_observation_evidence.py | 169 ++++++++++++++++++- 2 files changed, 262 insertions(+), 19 deletions(-) diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 3ac3c6f..b9b7210 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -1839,7 +1839,58 @@ def git_bytes(root, *arguments): return process.stdout -def verify_composition(root, roles, source_map, inputs, read_git=None): +_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), @@ -1865,6 +1916,7 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): 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)) @@ -1882,30 +1934,43 @@ def verify_composition(root, roles, source_map, inputs, read_git=None): 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') - with tested_checkout(root, roles, git) as view: - require(source_map == {p: row['sha256'] for p, row in runner.source_refs(view).items()}, + 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') - for path in sorted(required): + 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) - content = git('show', t + ':' + 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) + 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): +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', '') @@ -1913,6 +1978,7 @@ def tested_checkout(root, roles, read_git=None): 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 @@ -1921,10 +1987,20 @@ def tested_checkout(root, roles, read_git=None): 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') - target = view/path - target.parent.mkdir(parents=True, exist_ok=True) - target.write_bytes(git('show', tree + ':' + path.as_posix())) - yield view + 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): @@ -2164,7 +2240,7 @@ def check_tested(checkout, directory, root_roles, root): '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)) + 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') diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index c8c5c4d..1f147f9 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -183,6 +183,12 @@ class Repair28CompositionTests(unittest.TestCase): 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): @@ -190,7 +196,8 @@ class Repair28CompositionTests(unittest.TestCase): 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) + 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) @@ -252,6 +259,145 @@ class Repair28CompositionTests(unittest.TestCase): 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): @@ -274,6 +420,8 @@ class Repair30EntryTests(unittest.TestCase): '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') @@ -357,6 +505,25 @@ class Repair30EntryTests(unittest.TestCase): 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}')), From 93248f4d79a3be71d7a1ac0592e9219d405cd6b7 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 14:56:38 +0900 Subject: [PATCH 18/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution/completion_marker_contracts.json | 2 +- tests/ui/test_p23_life_observation.gd | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) 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 08749b3..9394c39 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 @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "610ff2f33ec0e42bdca55d692e640c9bd8fe9244b9b49b0263419b1998ff9e9e", + "source_sha256_at_schema_audit": "88c64fdb81937fc7dca24a87e74b568db8e35a3471c7f4ee7bc41c7d0ddeae24", "authority": { "path": "docs/spec/acceptance/P23-03.json", "scope": "component and read-only observation only; native unexecuted; whole unit blocked" diff --git a/tests/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index 9410247..e408808 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -6,6 +6,7 @@ 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: From 26f60095bff015efbf7d7cf89b5b0bbab1fec31e Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 17:15:06 +0900 Subject: [PATCH 19/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../execution/p23-03/REPAIR35_SOURCE_READY.md | 90 ++++++++++ scenes/main/world_preview.gd | 77 ++++++++- tests/ui/test_p23_life_observation.gd | 27 ++- tools/check_p23_life_observation_evidence.py | 156 ++++++++++++------ tools/run_p23_life_observation.py | 2 + tools/test_p23_life_observation_evidence.py | 121 ++++++++++++++ 7 files changed, 422 insertions(+), 53 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR35_SOURCE_READY.md 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 9394c39..2f7fe65 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 @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "88c64fdb81937fc7dca24a87e74b568db8e35a3471c7f4ee7bc41c7d0ddeae24", + "source_sha256_at_schema_audit": "5c52db4dd70f9702c096e918d849703d14aec1acb88bb2458ca01049da71ce3c", "authority": { "path": "docs/spec/acceptance/P23-03.json", "scope": "component and read-only observation only; native unexecuted; whole unit blocked" 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/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index 219834a..bac0f27 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -1561,6 +1561,50 @@ func _life_track_region(key: String, cell: Vector3i, model: Dictionary) -> Dicti 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"] @@ -1580,10 +1624,13 @@ func _life_actor_region(row: Dictionary, person: Dictionary, model: Dictionary) if model[&"carried_patients"].has(id): for carrier: Dictionary in model[&"staff"]: if carrier[&"id"] == model[&"carried_patients"][id]: - result = _life_track_region("staff/"+String(carrier[&"id"]), carrier[&"cell"], model) + 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): @@ -1600,6 +1647,8 @@ func _life_actor_region(row: Dictionary, person: Dictionary, model: Dictionary) 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 @@ -1680,6 +1729,7 @@ func _fit_life_actors(model: Dictionary) -> void: 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"]: @@ -1690,12 +1740,35 @@ func _fit_life_actors(model: Dictionary) -> void: 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 not region[&"transit"]: + 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 diff --git a/tests/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index e408808..fd56e60 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -10,19 +10,20 @@ class QueueWorld extends RefCounted: 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.x >= -1 and cell.x <= 2 + 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": 0.3, + &"width": 2, &"capacity": 4, &"boarded": [], &"visual_floor": visual_floor, &"operational": true, &"phase": &"PICKUP"}], &"trips": trips} class QueueRuntime extends RefCounted: @@ -52,6 +53,28 @@ func repair28_geometry() -> void: 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] diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index b9b7210..95b99f0 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -1455,6 +1455,40 @@ def actor_track_transit(report, key, cell): 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): runtime = state['systems']['runtime'] compare_queues(state, report) @@ -1467,6 +1501,9 @@ def compare_actor_geometry(state, report): 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] @@ -1489,11 +1526,62 @@ def compare_actor_geometry(state, report): 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 = track_position(key, cell) + 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']: @@ -1511,7 +1599,7 @@ def compare_actor_geometry(state, report): 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 = track_position(key, cell) + center = actor_region(key).get('center', track_position(key, cell))[:] special = True if key in report['queue_positions']: special = False @@ -1521,6 +1609,7 @@ def compare_actor_geometry(state, report): 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']: @@ -1555,55 +1644,16 @@ def compare_actor_geometry(state, report): 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'] - people = {'guest/'+r['guest_id']: r for r in runtime['guests']['guests']} - people.update({'staff/'+sid: r for sid, r in staff.items()}) - def tracked_bounds(key, cell): - track = report['actor_tracks'][key] - transit = actor_track_transit(report, key, cell) - rect = transform.rect(cell) - if transit: - other = transform.rect(track['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] - return rect, transit - members = [] + 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, {}) - region, transit = (cabin_bounds[key], False) if riding else tracked_bounds(key, cell) - fid = '' - if not riding: - if key in report['queue_positions']: - cx, cy = report['queue_positions'][key] - region, transit = [cx-transform.dim[0]/2, cy-height/2, transform.dim[0], height], False - elif not is_staff: - target = person['target_facility_id'] - if receiving and target in facilities and key.split('/', 1)[1] in report['rooms'].get(target, {}).get('people', []): - fid = target - else: - sid = person['staff_id'] - if sid in report['carried_patients']: - carrier = staff[report['carried_patients'][sid]] - region, transit = tracked_bounds('staff/'+carrier['staff_id'], carrier['current_cell']) - region[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 target in facilities 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: - region, transit = [rect[0]+slot*rect[2]/columns, rect[1], rect[2]/columns, rect[3]], False - if fid and occupancy.get(fid): - region, transit = transform.room(occupancy[fid]), False + 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] @@ -1650,13 +1700,23 @@ def compare_actor_geometry(state, report): frame=frame, variant=variant, flip=track.get('direction', 1) < 0, staff=is_staff, state=states.index(person['state']), receiving=receiving, carrying=carrying, riding=riding) - members.append(dict(row=row, bounds=[region[0]+p, region[1]+p, region[2]-2*p, region[3]-2*p], transit=transit)) + 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']} diff --git a/tools/run_p23_life_observation.py b/tools/run_p23_life_observation.py index 3e3364f..0a25514 100644 --- a/tools/run_p23_life_observation.py +++ b/tools/run_p23_life_observation.py @@ -311,6 +311,8 @@ def collect(args): 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') diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 1f147f9..68c294b 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -1848,6 +1848,127 @@ class AnchoredGeometryTests(unittest.TestCase): 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', From 059c2f743be655c11627a2b19d8eab9f86a000a6 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 19:18:11 +0900 Subject: [PATCH 20/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../execution/p23-03/REPAIR36_LOCAL_AUDIT.md | 69 ++++++++++++++++ scenes/main/world_preview.gd | 34 ++++++++ tests/ui/test_p23_life_observation.gd | 26 ++++++ tools/check_p23_life_observation_evidence.py | 77 +++++++++++++++++- tools/test_p23_life_observation_evidence.py | 81 +++++++++++++++++++ 6 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md 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 2f7fe65..8820fb1 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 @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, failures", "required": true, "fixed_check_count_used": false, - "source_sha256_at_schema_audit": "5c52db4dd70f9702c096e918d849703d14aec1acb88bb2458ca01049da71ce3c", + "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" 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/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index bac0f27..fa32fe0 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -637,6 +637,7 @@ func _build_life_observation() -> Dictionary: &"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"]) @@ -691,6 +692,39 @@ func _traffic_segments(band: Rect2, cell: Rect2, obstacles: Array[Rect2]) -> Arr 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) diff --git a/tests/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index fd56e60..6cf5c50 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -100,6 +100,31 @@ func repair28_geometry() -> void: 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: @@ -118,6 +143,7 @@ func run() -> void: 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(): diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 95b99f0..8a62d1e 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -310,6 +310,9 @@ def paired_crop_noise(before, after, rect): 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' @@ -549,13 +552,16 @@ def write_frame_review(directory, frame, provenance): return manifest, digest -def reviewed_pixel_categories(manifest, digest, verdict_path): +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') @@ -651,7 +657,7 @@ def inspect_offline_mirror(directory): 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') + 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']) @@ -803,6 +809,61 @@ 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 @@ -965,6 +1026,9 @@ def validate_life_overlay(report, root=ROOT, snapshot=None): 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') @@ -1222,6 +1286,9 @@ def compare_goods(snapshot, report, root=ROOT): 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 = [] @@ -1490,6 +1557,9 @@ def landing_actor_region(state, report, transform, cell, region, transit): 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) @@ -1800,6 +1870,7 @@ def inspect_stream(directory, root=ROOT): 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) @@ -2240,7 +2311,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT): 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')) + 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 diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 68c294b..2b8b1f1 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -2293,5 +2293,86 @@ class FrameReviewTests(unittest.TestCase): self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set()) +class Repair36LabelTests(unittest.TestCase): + inputs = check.ROOT.parent / '.runtime/P23-03/build/audit-support/badge-repair36-exact-inputs' + + def recorded(self): + request = json.loads((self.inputs/'bad-native-request/request.json').read_bytes()) + stream = self.inputs.parent.parent/'p23-normal-os-26f-r17/os-pair/B/writer-stream' + capture = next(json.loads(line) for line in (stream/'manifest.jsonl').read_text().splitlines() + if json.loads(line).get('label') == 'writer-request-0021') + return request, check.read_object(stream, capture['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.inputs/'bad-native-request/review-image-0/manifest.json').read_bytes()) + 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() From d2581afd26d1976cdfe4f44c89df49a772524d1e Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 19:28:26 +0900 Subject: [PATCH 21/21] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE?= =?UTF-8?q?=E8=A6=B3=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../p23-03/REPAIR37_PORTABLE_FIXTURE.md | 21 ++++++++++++ .../p23-03/recorded-badge-negative.json.gz | Bin 0 -> 83518 bytes tools/test_p23_life_observation_evidence.py | 30 ++++++++++++++---- 3 files changed, 44 insertions(+), 7 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR37_PORTABLE_FIXTURE.md create mode 100644 tests/fixtures/p23-03/recorded-badge-negative.json.gz 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/tests/fixtures/p23-03/recorded-badge-negative.json.gz b/tests/fixtures/p23-03/recorded-badge-negative.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..510726689fdda1f4175850465e7e755c6fd797e4 GIT binary patch literal 83518 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