P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-16 14:17:14 +09:00
parent 9a26f9ab8c
commit 6c13940cf4
10 changed files with 724 additions and 50 deletions
@@ -3707,7 +3707,7 @@
"template": "P23 life observation: <checks> checks, <failures> 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"
@@ -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.
+66 -21
View File
@@ -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():
+5 -1
View File
@@ -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:
+1 -1
View File
@@ -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:
+14
View File
@@ -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."
}
+70 -1
View File
@@ -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.
+229 -12
View File
@@ -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
+8 -2
View File
@@ -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)
+244 -11
View File
@@ -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)