From 6c13940cf4321548491495fded26b82645aa58da Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 14:17:14 +0900 Subject: [PATCH] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE=E8=A6=B3?= =?UTF-8?q?=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)