P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-15 09:36:36 +09:00
parent 03100beabe
commit 4c8a18aa75
11 changed files with 2207 additions and 35 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": "b9afa66ec73d3a5cadf86f9a71f0d13f5b085c3cc9a534dfe29b2bed0e2e76a0",
"source_sha256_at_schema_audit": "cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1",
"authority": {
"path": "docs/spec/acceptance/P23-03.json",
"scope": "component and read-only observation only; native unexecuted; whole unit blocked"
@@ -1,3 +1,205 @@
# P23-03 worker status — static-complete submission
Contract base: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
This finalization preserves every pre-existing modified and untracked file.
Static-complete is supported by the wiring inspection and local checks below.
No acceptance claim is made. No Godot, native, OS collector, Runner, full
regression, git write, branch change, or other worker was used.
## Current wiring confirmed by git diff and rg only
Line numbers refer to the current candidate.
- Checker `tools/check_p23_life_observation_evidence.py`: `check` at 1189
reaches `inspect_native_phases` (1162) at 1215 and `inspect_stream` (950)
at 1220/1226. The stream inspection invokes `compare_rest` (531) at 968,
`compare_motion` (738) at 974, and `compare_actor_geometry` (800) at 975,
alongside actor, room, queue, traffic, goods, badge and cabin checks.
- `check` reaches `inspect_os_receipt` (1038), `inspect_os_requests` (1092),
and `inspect_resume` (1070) at 1249, 1250 and 1257.
Request inspection calls `screenshot_pixels` (184) at 1143, which invokes
`inspect_conversion` (158) at 191 for JPEG evidence; rectangle/pixel
checking follows at 1154.
- `check` builds executed check sets at 1270–1272, then calls `BLOCKERS`
at 1273, reaching `condition_matrix` (982) through 1000. All original
seven CONDITIONS and six CATEGORIES (room, queue, EV, staff, goods,
business state, represented by the key `business`) remain required.
Missing executed check names produce pending/deficits; no fixed-true matrix.
- Collector `tools/run_p23_life_observation.py`: `collect_os` (110)
invokes `collect_screenshots` (26) at 186. JPEG collection invokes
`convert_screenshot` (73) at 55; conversion calls `inspect_conversion`
at 107. Raw CUA bytes and tool-call bindings are retained separately from
converted PNG and conversion receipts. This wiring was inspected, not run.
- `diagnostic_stream.gd` captures hash-addressed `rest_audit` and `motion`
at 70–78. `rest_audit` (89) records selection inputs, route/cost probes,
locks/transit and comparison-only Runtime output. No stored-rest field was
introduced. The source-grounded correction supersedes the old stored-state
proposal below. MotionObserver retains two draw frames, not whole Runtime
state per frame.
- Native route: `tests/ui/test_p23_life_observation.gd` at 18–21 invokes
`layout_comparison.gd`; the runner invokes that UI test at 309 for A/B.
Layout comparison preloads diagnostics at 5 and starts watching at 65.
- Normal OS route is separate from the native UI component test: runner
RECORDER at 24 is copied/launched at 145/152; `passive_os_recorder.gd`
preloads `passive_observation.gd` at 2 and calls its inherited run at 6.
`passive_observation.gd` extends `observation.gd` at 1;
`observation.gd` preloads diagnostics/layout comparison at 3–4 and
watches the real preview at 26. These fixtures are reached through the
OS runner, not directly preloaded by the native UI test.
- Existing mutation negatives cover conversion provenance, rest operands,
motion history, rectangle/pixel transforms, process/source/profile guards,
formal members/distinct PID/ACK, and every condition/category matrix cell.
No concrete missing call or negative was found; no code hardening was added.
## Exact local commands and real results
- `git status --short`, `git diff --stat`, and targeted `git diff --`
for the collector, UI test, diagnostic fixture and marker: inspected existing
changes without modifying git state.
- Targeted `rg -n` / `rg -n -A...` searches for function definitions,
calls, fixture preloads/extends, rest_audit/motion, CONDITIONS/CATEGORIES
and unittest methods in the three P23 tools and five P23 fixtures/UI test:
wiring results recorded above. No Runtime/source audit or Phase0 repetition.
- `python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
initial attempt failed because the interpreter's default cache destination
under Library/Caches was not writable; no syntax failure was reported.
- `PYTHONPYCACHEPREFIX=docs/plans/original-completion-2026-10-02/execution/p23-03/.finalization-pycache python3 -m py_compile tools/run_p23_life_observation.py tools/check_p23_life_observation_evidence.py tools/test_p23_life_observation_evidence.py`:
PASS, exit 0 for all three tools.
- `python3 -B tools/test_p23_life_observation_evidence.py`:
**27 test methods run, 27 passed**, 0 failures/errors; reported 0.044 seconds.
These are synthetic checker tests, not native/OS evidence.
- `git diff --check`: PASS, exit 0; repeated after this status rewrite.
- Read-only `python3 -B -c` JSON/hash/path assertions using
`git show 860136f7099f5e3aee99da5798a627a77f2d8eea:<path>`,
`git diff --name-only <base>` and
`git ls-files --others --exclude-standard`: PASS.
All old marker metadata/rows equal base; exactly one appended P23-03
required source-authority entry; test SHA
`cc7049e1c3919585caeefd1747be12fbf3fa52141fcdb980929c8a16fd7e2ee1`
matches current bytes. suite-policy.json equals base byte-for-byte.
All 16 changed/untracked paths relative to base are allowed.
- The immutable spec SHA matches
`8568457df849ef587c731f072d77244ef5420fc4e1190c5069cfd6cfeea9c89f`
using the existing checker's `read_text().strip().encode()` convention.
An initial extra raw-byte SHA assertion failed (raw file SHA
`1a925e0d7f5c1c9f28670d29dd0fdc5aa48f4d5415946d6de4c15e1d83b5ed66`);
normalized verification passed and the spec has no diff against base.
- An initial inline validation here-document could not create its shell
temporary file; rerun via `python3 -B -c` succeeded.
- Removed only the temporary .finalization-pycache tree created by this run.
No pre-existing dirty/untracked material was removed.
## Root work still pending — expected, not worker blockers
Native Godot execution; actual OS collector with screenshots for both layouts
and all six categories, FRONT/BACK and ordinary pan/zoom; formal save and
distinct actual PID normal resume; one full exact-candidate **177-suite**
regression; independent Opus all-condition review. These must precede any
acceptance determination. Constructibility, visual correctness and same-demand
improvement have not been claimed from static checks.
## Superseded history — retained verbatim
The following earlier blocked statuses (including the 19-method and
"not yet wired" claims) describe previous candidates and are superseded by the
current wiring and results above.
# Current RUN continuation — blocked on stored-rest-destination contract
All seven pre-existing modified files and three untracked fixtures were retained.
No git writes, agents, Godot, collector execution, Runner or full regression.
This is not a genuine completed submission and makes no acceptance claim.
Root native/OS evidence remains **pending**, independently of this source blocker.
Changes completed in this continuation:
- Removed mandatory left-button drag and its pan-change assertion. Camera
screenshots now use ordinary-wheel-zoom, pan-mode-button, move-mode-wheel-pan.
Existing non-Ctrl event observation is preserved.
- Added stdlib PNG decoding with CRC/chunk/end checks, RGB/RGBA and filters 0–4.
JPEG dimensions reuse the P08 helper; no JPEG decoder or PIL was added.
- Added conversion receipt inspection and Root-only convert_screenshot helper:
separate output, original CUA hash/call binding, sips executable SHA/argv,
output digests, decoded PNG and magic-derived dimension equality. No actual
sips conversion was run here. This helper is not yet wired into collect_os.
- Added four image test methods including all filters, CRC corruption, trailing
data, converter SHA/argv, raw CUA hash, dimensions and missing-conversion negatives.
Actually executed: 19 checker unittest methods passed; py_compile passed for
the three P23 Python tools. Temporary compilation outputs were removed.
Synthetic image tests are checker tests, never native screenshot evidence.
Concrete contract gap (targeted dependency inspection, not a repeated Phase0 audit):
- autoload/simulation_runtime.gd:13109–13125 computes get_staff_rest_report by
scanning facilities and selecting a minimum-cost route on every call. The
selected facility and eligibility are local variables, not stored state.
- autoload/simulation_runtime.gd:8783–8785 calls that query and passes only its
cell to advance_staff_commute. At :12656–12690 the movement function either
returns without movement or emits a STAFF_MOVED event with a target_cell;
it does not persist a selected rest facility ID or eligibility. Movement
events cannot supply a current destination for stationary/nonmoving staff.
- scripts/staff/staff_runtime.gd:282–315 serializes current_cell, assignment,
task and path_handle but no rest destination/eligibility. Runtime snapshot
assembly at :511–570 has no stored rest-selection model. PathService's
snapshot at scripts/world/path_service.gd:1606 stores path/actor projections,
not the selected rest facility and its eligibility.
Thus recipe (3), "rest destination must equal the typed stored destination and
its eligibility; do not re-simulate AI", has no authoritative stored operand.
Calling get_staff_rest_report from a fixture would rerun selection, and copying
its output into a purported stored-state field would manufacture authority.
These Runtime/staff/path files are outside allowed_paths and remain unchanged.
Read source SHA256:
- Runtime: 4696df4a77feb465a5a3ba8b806569f7bcbe1d03c88289acfb78a3928f307d67
- StaffRuntime: 807a417bc7866c97cd6d8f5c1368b5480a2d9fd685a2a47a978e81482bf522b5
- PathService: 447135729a1106639a6eee6754af3195c8a8b458ca26980ae91fa39d80dcfbd4
Minimal separate proposal for Root/Opus, not applied: authorize an authoritative
rest-decision record at the existing Runtime selection point, including selected
facility/cell, eligibility and decision time, with explicit invalidation and
snapshot/restore semantics. Alternatively Root must identify an existing typed
stored field that has those semantics. No worker relaxation of this gate.
Remaining source implementation is explicit: collector conversion integration,
all row/rectangle bindings, motion history, distinct-PID/formal ACK checker,
dynamic matrix and the remaining requested negatives. Existing fail-closed
whole-unit behavior remains; no unconditional PASS or new fail stub was added.
The 30/88 denominator, seven conditions, six categories and old suite policies
are unchanged. Do not begin acceptance assuming only Root evidence is missing.
---
Previous status retained as history:
# Repair from source 03100be — blocked, implementation incomplete
Source checkout HEAD confirmed read-only: `03100beabeb55caac03f53db3d1f399b424df517`.
Contract base remains `860136f7099f5e3aee99da5798a627a77f2d8eea`.
No git writes, Godot, Runner, OS input, regression, or other workers were used.
No acceptance or partial credit is claimed. No out-of-scope defect or missing Runtime capability was established.
Changes in this repair:
- Added registered `--verify-only` CLI, concrete missing-index deficits, immutable fixture/spec/contract/baseline/AGENTS pins and a normative P23 plan-section hash. Full-plan SHA is separate provenance only. Existing diagnostic/layout CLI modes retained.
- Removed fixture-side begin_leaving/despawn_guest calls; Runtime owns continued guest behavior. Added A/B native entry to the existing component test and updated the existing single marker row SHA.
- Added observation.gd extending P08 placement helpers unchanged, with real supply scenario commands/settings, common definition-derived construction budget, seed806042, relocated common cafeteria/kitchen at x6 to avoid declared B elevator footprint, sparse typed captures, normal save/load and whole formal-model comparisons. These construction prechecks have not been executed.
- Added passive request/ACK endpoint and recorder for four layout/role combinations, live FRONT/BACK buttons and PanModeButton, ordinary non-Ctrl wheel zoom/pan and ordinary pan drag only. No deferred P23-02 held-gesture credit.
- Replaced the collector stub with an adaptation of P08 collect/collect_os using run_all_tests helpers and owned gates, fresh outputs/profiles, native import/parse/layout phases, four OS roles and formal reader copies. Records the current interpreter PIL availability; raw screenshot bytes are left untouched. Collector has not been executed.
- Checker now inspects available native/OS streams, comparisons and basic process/source/profile guards, but explicitly fails closed for outstanding whole-unit gates.
Local verification actually executed: 16 Python unittest cases PASS; ast.parse for the three changed Python tools PASS; one P23 marker row and exact source SHA PASS. No engine or evidence-collector execution.
Remaining IMPLEMENTATION, separate from missing Root evidence:
1. Complete screenshot byte-magic dimensions, stdlib PNG pixel/rect checks, and indexed Root JPEG-to-lossless-PNG conversion binding to raw hash/converter provenance. PIL absence is not waived.
2. Add prior-frame visual-position/frame-delta evidence and independent interpolation checks; compare rest destinations with typed stored destination/eligibility without re-simulating AI.
3. Bind all request report rows to viewport rectangles/affine and screenshots; current endpoint retains report plus viewport-to-screen transform but does not yet produce every required viewport row rectangle.
4. Finish independent distinct-PID/formal-member/resume-state/load-ACK gates. Fixture assertions do not replace checker proof.
5. Replace hard failure condition-matrix deficits with exact completed check names only after all seven independent gates exist; finish all requested category and infrastructure negatives. Current check intentionally cannot pass whole-unit integrity.
6. Statically review collector lifecycle/failure receipts and fixture category coverage; real constructibility and EV/passenger/queue/goods coverage remain unproved.
Root native, actual OS for A/B, formal distinct-PID resume, exact-final full regression, independent review, integration/final/ledger: all PENDING. This candidate is blocked on remaining in-scope implementation, not ready for acceptance or a claim that missing Root evidence is the only gap.
---
Previous worker status retained verbatim as history (superseded by the repair status above):
# P23-03 worker status: submitted static work, implementation incomplete
Base commit: `860136f7099f5e3aee99da5798a627a77f2d8eea`.
+88 -2
View File
@@ -3,6 +3,40 @@ extends RefCounted
const Codec := preload("res://tests/fixtures/p08-02/mixed_world.gd")
var directory: String
var manifest: FileAccess
var observer: MotionObserver
class MotionObserver extends Node:
var view: WorldPreview
var frames: Array = []
var elapsed := 0.0
var scaled_elapsed := 0.0
func _ready() -> void:
RenderingServer.frame_post_draw.connect(after_draw)
func _process(delta: float) -> void:
elapsed += delta
var clock := get_node_or_null("/root/GameClock")
if clock == null or not bool(clock.get("paused")):
scaled_elapsed += delta * maxf(float(clock.get("speed")) if clock != null else 1.0, 0.0)
func after_draw() -> void:
if not is_instance_valid(view): return
var report := view.get_life_observation_report()
if report.is_empty(): return
var sample := {}
for key: String in ["animation_time", "paused", "layer", "zoom", "pan", "viewport", "actor_tracks", "elevator_tracks", "actor_drawings", "cabins"]:
sample[key] = report[key]
if not frames.is_empty() and sample == frames[-1]["display"] and not sample["paused"]: return
# Keep only two draw frames, never whole Runtime copies per frame.
frames.append({"frame": Engine.get_process_frames(), "delta": elapsed, "scaled_delta": scaled_elapsed, "display": sample})
if frames.size() > 2: frames.pop_front()
elapsed = 0.0
scaled_elapsed = 0.0
func _exit_tree() -> void:
if RenderingServer.frame_post_draw.is_connected(after_draw): RenderingServer.frame_post_draw.disconnect(after_draw)
func watch(view: WorldPreview) -> void:
observer = MotionObserver.new()
observer.view = view
view.add_child(observer)
func open(path: String) -> bool:
directory = path
@@ -33,19 +67,71 @@ func capture(label: String, runtime: Variant, view: WorldPreview) -> bool:
var report := view.get_life_observation_report()
var state_hash := object(state)
var report_hash := object(report)
if state_hash.is_empty() or report_hash.is_empty():
var audit_hash := object(rest_audit(runtime))
var motion_hash := object({"frames": observer.frames.duplicate(true) if is_instance_valid(observer) else []})
var after: Dictionary = runtime.create_snapshot()
var after_hash := object(after)
if state_hash.is_empty() or report_hash.is_empty() or audit_hash.is_empty():
return false
manifest.store_line(JSON.stringify({"label": label, "pid": OS.get_process_id(),
"ticks_usec": Time.get_ticks_usec(), "state": state_hash, "report": report_hash,
"rest_audit": audit_hash, "motion": motion_hash, "state_after": after_hash,
"input_method": "diagnostic", "acceptance": "pending"}))
manifest.flush()
return var_to_bytes(state) == var_to_bytes(runtime.create_snapshot())
return var_to_bytes(state) == var_to_bytes(after)
func close() -> void:
if manifest != null:
manifest.close()
manifest = null
# Public selection inputs; Runtime output is only a comparison operand.
func rest_audit(runtime: Variant) -> Dictionary:
var rows: Array = []
for member: StaffRuntime in runtime.staff_service.get_staff_members():
var memberships: Array = []
for raid_id: StringName in runtime.defense_service.get_raid_ids():
var raid: Dictionary = runtime.defense_service.get_raid(raid_id)
if raid.get(&"combatants", {}).has(member.staff_id):
memberships.append({"raid_id": raid_id, "state": DefenseService.RaidState.keys()[raid[&"state"]],
"side": raid[&"combatants"][member.staff_id].get(&"side", &"")})
var facilities: Array = []
for room: FacilityInstance in runtime.facility_service.get_instances():
var probes: Array = []
var candidates: Dictionary = {}
var occupied := room.occupied_cells.duplicate()
if occupied.is_empty(): occupied.append(room.origin)
for cell: Vector3i in occupied:
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT]:
var present: bool = runtime.path_service.has_cell(cell + delta)
probes.append({"cell": cell + delta, "has_cell": present})
if present: candidates[cell + delta] = true
var ordered: Array = candidates.keys()
ordered.sort_custom(func(a: Vector3i, b: Vector3i) -> bool:
if a.z != b.z: return a.z < b.z
if a.y != b.y: return a.y < b.y
return a.x < b.x)
var routes: Array = []
for cell: Vector3i in ordered:
var path: Array = runtime.path_service.find_path(member.current_cell, cell,
[WorldService.LAYER_FRONT, WorldService.LAYER_BACK], [&"staff"])
var edges: Array = []
for index: int in range(1, path.size()):
# Empty reports remain explicit mismatch evidence.
edges.append(runtime.path_service.get_edge_report(path[index - 1], path[index], [&"staff"]))
routes.append({"cell": cell, "path": path, "edges": edges,
"calculate_path_cost": runtime.path_service.calculate_path_cost(path, [&"staff"]) if not path.is_empty() else -1})
facilities.append({"instance_id": room.instance_id, "definition_id": room.definition_id,
"operation_state": FacilityInstance.OperationState.keys()[room.operation_state],
"origin": room.origin, "occupied_cells": room.occupied_cells.duplicate(),
"probes": probes, "ordered": ordered, "routes": routes})
rows.append({"staff_id": member.staff_id, "state": StaffRuntime.State.keys()[member.state],
"health": member.health, "current_task_id": member.current_task_id, "current_cell": member.current_cell,
"defense_memberships": memberships, "actor_lock": runtime.event_service.get_actor_locks().has(member.staff_id),
"in_transit": runtime.is_staff_in_transit(member.staff_id), "facilities": facilities,
"comparison_only": runtime.get_staff_rest_report(member.staff_id)})
return {"staff": rows, "path_service_sha256": FileAccess.get_sha256("res://scripts/world/path_service.gd")}
func record(label: String, value: Dictionary) -> bool:
if manifest == null: return false
var digest := object(value)
+7 -7
View File
@@ -62,6 +62,7 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool:
view.size = Vector2(1440, 810)
view.set_runtime(runtime)
tree.root.add_child(view)
stream.watch(view)
var finished: Dictionary = {}
for offset: int in range(WINDOW + 1):
if not ok: break
@@ -77,20 +78,19 @@ func run(tree: SceneTree, directory: String, layout: String) -> bool:
ok = stream.record("arrival-%s" % arrival[&"id"], {&"offset": offset, &"payload": payload, &"result": result.to_dictionary()}) and result.is_success() and ok
# Typed per-minute projections retain paths, queues, density and actual
# completed services. Whole aggregate snapshots are only sampled sparsely.
var guest_records: Dictionary = runtime.guest_service.create_snapshot()
ok = stream.record("tick-%03d" % offset, {&"minute": clock.absolute_minute,
&"guests": runtime.guest_service.create_snapshot(), &"path": runtime.path_service.create_snapshot(),
&"guests": guest_records, &"path": runtime.path_service.create_snapshot(),
&"elevators": runtime.elevators.create_snapshot(), &"facilities": runtime.facility_service.create_snapshot()}) and ok
for guest: GuestRuntime in runtime.guest_service.get_guests():
if guest.current_cell != GOAL or finished.has(guest.guest_id): continue
finished[guest.guest_id] = offset
var leaving: Dictionary = runtime.guest_service.begin_leaving(guest.guest_id, clock.absolute_minute, &"COMPARISON_DESTINATION_REACHED")
var despawn: Dictionary = runtime.guest_service.despawn_guest(guest.guest_id, clock.absolute_minute)
ok = bool(leaving.get(&"ok", false)) and bool(despawn.get(&"ok", false)) and ok
for guest: Dictionary in guest_records[&"guests"]:
if guest[&"current_cell"] != GOAL or finished.has(guest[&"guest_id"]): continue
finished[guest[&"guest_id"]] = offset
if offset % 30 == 0:
view.queue_redraw()
await tree.process_frame
await tree.process_frame
ok = stream.capture("frame-%03d" % offset, runtime, view) and ok
ok = stream.record("first-arrivals", {&"offsets": finished}) and ok
ok = stream.record("final", runtime.create_snapshot()) and ok
stream.close()
view.free()
+161
View File
@@ -0,0 +1,161 @@
extends "res://tests/fixtures/p08-03/placement.gd"
## Real Runtime commands; inherited formal codec/save-operation helpers unchanged.
const Stream := preload("res://tests/fixtures/p23-03/diagnostic_stream.gd")
const Comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd")
var stream: RefCounted
var layout := "A"
var facility_ids: Dictionary = {}
func setup() -> void:
clock.reset(1, 6, 0)
var state: Variant = tree.root.get_node("GameState")
state.reset()
expect(runtime.initialize(clock, state, 806042).get(&"valid", false), "initialize observation")
clock.set_paused(true)
tree.root.size = Vector2i(1440, 810)
layout = OS.get_environment("P23_LAYOUT")
expect(layout in ["A", "B"], "declared layout A or B")
main = load("res://scenes/main/main.tscn").instantiate()
main.manual_save_path = "user://saves/manual_ui"
if role == "reader":
import_package(directory.path_join("formal"), String(main.manual_save_path))
tree.root.add_child(main)
runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false})
stream = Stream.new()
expect(stream.open(directory.path_join(role + "-stream")), "fresh observation stream")
stream.watch(main.world_preview)
await frames()
func command(id: StringName, payload: Dictionary) -> bool:
var result: CommandResult = runtime.submit_command(id, payload)
expect(result.is_success(), String(id) + ": " + String(result.code))
expect(stream.record("command", {&"command": id, &"payload": payload,
&"result": result.to_dictionary(), &"minute": clock.absolute_minute}), "record public command")
return result.is_success()
func advance(minutes: int) -> void:
for unused: int in range(minutes):
clock.advance_minutes(1)
await frames()
await observe_progress()
if clock.absolute_minute % 30 == 0:
expect(stream.capture("minute-%d" % clock.absolute_minute, runtime, main.world_preview), "sparse minute observation")
func observe_progress() -> void:
pass
func prepare() -> void:
# Both layouts receive the same definition-derived diagnostic construction budget.
# Cafeteria/kitchen move together to x=6, leaving x=4..5 for the declared B EV.
var builds: Array = [[&"reception", Vector3i(-8, 0, 0)],
[&"cafeteria", Vector3i(6, 0, 0)], [&"warehouse", Vector3i(-8, 0, 1)],
[&"kitchen", Vector3i(6, 0, 1)], [&"staff_door", Vector3i(0, 0, 1)],
[&"standard_room", Vector3i(-8, 2, 0)], [&"guest_stairs", Vector3i(-4, 1, 0)],
[&"guest_stairs", Vector3i(-4, 2, 0)], [&"guest_corridor", Vector3i(-5, 2, 0)],
[&"guest_corridor", Vector3i(-6, 2, 0)], [&"guest_elevator", Vector3i.ZERO],
[&"guest_corridor", Vector3i(2, 1, 0)], [&"guest_corridor", Vector3i(3, 1, 0)],
[&"staff_waiting_room", Vector3i(-4, 0, 1)]]
var improvement: Dictionary = Comparison.new().declaration()[&"improvement"]
var budget_builds := builds.duplicate(true)
budget_builds.append([improvement[&"definition_id"], improvement[&"origin"]])
var needed := {&"gold": 0, &"materials": 0}
var costs: Array = []
for row: Array in budget_builds:
var definition: FacilityDefinition = runtime.definition_registry.get_facility(row[0])
costs.append({&"definition": row[0], &"cost": definition.construction_cost.duplicate(true)})
for key: StringName in needed:
needed[key] += int(definition.construction_cost.get(key, 0))
var state: Variant = tree.root.get_node("GameState")
var deltas := {}
for key: StringName in needed:
if needed[key] > state.get_resource(key):
deltas[key] = needed[key] - state.get_resource(key)
budget = {"setup": "diagnostic", "costs": costs, "needed": needed,
"initial": state.resources.duplicate(true), "deltas": deltas}
if not deltas.is_empty(): command(&"change_resources", {&"deltas": deltas})
expect(stream.record("budget", budget), "common diagnostic budget")
if layout == "B": builds.append([improvement[&"definition_id"], improvement[&"origin"]])
# Preserve the supply scenario's staged support/construction sequence.
for index: int in range(builds.size()):
if index == 5: await advance(180)
if index == 7: await advance(60)
if index == 8: await advance(10)
command(&"build_facility", {&"definition_id": builds[index][0], &"origin": builds[index][1]})
var transport_minutes := 0
for id: StringName in [&"guest_elevator", &"guest_corridor"]:
transport_minutes = maxi(transport_minutes, runtime.definition_registry.get_facility(id).build_minutes)
await advance(maxi(10, transport_minutes))
for facility: FacilityInstance in runtime.facility_service.get_instances():
facility_ids[facility.definition_id] = facility.instance_id
command(&"set_facility_conditions", {&"facility_id": facility_ids[&"cafeteria"],
&"conditions": {&"related_facility_id": facility_ids[&"kitchen"]}})
for assignment: Array in [[&"staff_akane", &"reception"], [&"staff_gina", &"warehouse"],
[&"staff_chika", &"kitchen"], [&"staff_iris", &"cafeteria"], [&"staff_juno", &"cafeteria"]]:
command(&"hire_staff_candidate", {&"candidate_id": assignment[0]})
command(&"set_staff_work_assignment", {&"staff_id": assignment[0],
&"facility_id": facility_ids[assignment[1]], &"template_id": &"DAY"})
command(&"purchase_supplies", {&"warehouse_id": facility_ids[&"warehouse"],
&"item_id": SupplyCatalog.RAW_FOOD, &"quantity": 40})
command(&"set_kitchen_production", {&"facility_id": facility_ids[&"kitchen"],
&"enabled": true, &"target_stock": 8})
# The OS observation uses the already declared transport cohort, plus
# ordinary public arrivals whose service choice remains Runtime-owned.
# These commands are recorded and identical across A/B; no actor is moved,
# despawned, or inserted into a room/queue by the fixture.
var demand: Dictionary = Comparison.new().declaration()
expect(stream.record("observation-demand", {"cohort": demand[&"arrivals"],
"schedule": "all observation actors at the recorded current minute; offsets apply only to native layout comparison",
"minute": clock.absolute_minute}), "declared observation demand")
for arrival: Dictionary in demand[&"arrivals"]:
command(&"spawn_guest", {&"guest_id": StringName("os-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100,
&"options": {&"current_cell": Comparison.START, &"state": GuestRuntime.State.MOVING,
&"state_entered_at": clock.absolute_minute - 1, &"planned_exit_at": clock.absolute_minute + 2000,
&"path_handle": {&"target_cell": Comparison.GOAL}}})
command(&"spawn_guest", {&"guest_id": StringName("service-" + String(arrival[&"id"])),
&"guest_type_id": &"traveler", &"gold": 100})
await advance(120)
func layer_observations(prefix: String) -> void:
await button(main.front_button)
expect(stream.capture(prefix + "-FRONT", runtime, main.world_preview), "FRONT state/report")
await button(main.back_button)
expect(stream.capture(prefix + "-BACK", runtime, main.world_preview), "BACK state/report")
func writer() -> void:
await prepare()
await layer_observations("writer")
capture("presave")
await button(main.find_child("ManualSaveButton", true, false))
expect(main.build_status_label.text == "手動セーブを保存しました。", "normal save")
capture("checkpoint")
export_package(String(main.manual_save_path), "formal")
var formal := inspect_formal()
var presave := FileAccess.get_file_as_bytes(directory.path_join("writer-presave.var"))
expect(presave == var_to_bytes(formal), "whole presave formal model")
expect(not operation_results.is_empty(), "save result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_CHECKPOINT_SUCCEEDED")
var members := {}
for member: String in MEMBERS:
members[member] = FileAccess.get_sha256(directory.path_join("formal/" + member))
json_file("save.json", {"pid": OS.get_process_id(), "members": members})
func reader(distinct: bool) -> void:
var saved: Dictionary = JSON.parse_string(FileAccess.get_file_as_string(directory.path_join("save.json")))
expect(distinct and saved.pid != OS.get_process_id(), "distinct actual reader PID")
await load_menu(main.find_child("SaveLoadMenu", true, false))
await frames()
expect(main.build_status_label.text == "手動セーブを読み込みました。", "normal resume")
capture("checkpoint")
var formal := inspect_formal()
expect(FileAccess.get_file_as_bytes(directory.path_join("writer-formal-model.var")) == var_to_bytes(formal), "whole formal resume model")
for member: String in MEMBERS:
expect(FileAccess.get_sha256(directory.path_join("formal/" + member)) == saved.members[member], "formal member " + member)
expect(not operation_results.is_empty(), "load result recorded")
if not operation_results.is_empty():
verify_operation(formal, operation_results.back(), &"SAVE_RECOVERY_LOADED")
await layer_observations("reader")
func finish_observations() -> void:
stream.close()
+254
View File
@@ -0,0 +1,254 @@
extends "res://tests/fixtures/p23-03/observation.gd"
## Passive endpoint implementation. All gameplay/input is performed by Root CUA.
## Shared parent contains the handler endpoints, but every such endpoint below
## is overridden; this class never emits UI signals or invokes product handlers.
var receipts: Array = []
var terminated := false
var terminal_record: Dictionary = {}
var terminal_cause_recorded := false
var reveal_rows: Array = []
var all_requests: Array = []
var input_observer: Node
var last_dispatch: Dictionary = {}
var observed_categories: Dictionary = {}
func observe_progress() -> void:
if role != "writer" or terminated: return
var wanted: Dictionary = {}
var report: Dictionary = main.world_preview.get_life_observation_report()
for room: Dictionary in report.get(&"rooms", {}).values():
if int(room.get(&"count", 0)) > 0: wanted["room-users"] = true
for badge: Dictionary in report.get(&"landing_badges", []):
if int(badge.get(&"overflow", 0)) > 0: wanted["queue-overflow"] = true
for cabin: Dictionary in report.get(&"cabins", []):
if not cabin[&"passengers"].is_empty(): wanted["EV-passengers"] = true
var elevators: Dictionary = runtime.elevators.get_report()
for shaft: Dictionary in elevators.get(&"shafts", []):
if not shaft.get(&"boarded", []).is_empty(): wanted["EV-passengers"] = true
for trip: Dictionary in elevators.get(&"trips", []):
if trip.get(&"state", &"") == &"QUEUED": wanted["queue-present"] = true
for member: StaffRuntime in runtime.staff_service.get_staff_members():
if not member.carried_inventory_id.is_empty(): wanted["carried-goods"] = true
if not report.get(&"resting_staff", {}).is_empty(): wanted["rest-position"] = true
for category: String in wanted:
if observed_categories.has(category): continue
observed_categories[category] = true
await layer_observations("actual-" + category)
if category == "EV-passengers": await motion_observations()
func motion_observations() -> void:
await button(main.front_button)
await button(main.pause_button)
expect(not clock.paused, "ordinary resume for motion observation")
var minute: int = clock.absolute_minute
while clock.absolute_minute == minute and not terminated:
await tree.process_frame
await frames()
expect(stream.capture("active-motion", runtime, main.world_preview), "active interpolation history")
await button(main.pause_button)
expect(clock.paused, "ordinary pause for stable observation")
await frames()
expect(stream.capture("paused-motion", runtime, main.world_preview), "paused interpolation history")
class InputObserver extends Node:
var events: Array = []
var endpoint: RefCounted = null
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton or event is InputEventKey or event is InputEventMouseMotion:
var row := {"ticks_usec": Time.get_ticks_usec(), "text": event.as_text(), "pressed": event.is_pressed()}
if event is InputEventMouseButton:
row["button"] = event.button_index
row["position"] = [event.position.x, event.position.y]
row["ctrl"] = event.ctrl_pressed
if endpoint != null and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
row["dispatch"] = endpoint.dispatch_position(event.position)
events.append(row)
func expect(ok: bool, message: String) -> void:
if terminated and ok:
return
if terminated and terminal_cause_recorded:
if not ok:
terminal_record["suppressed_after_terminal"] += 1
json_file(role + "-terminal.json", terminal_record)
return
if terminated and not ok:
terminal_cause_recorded = true
super.expect(ok, message)
func terminate(stem: String, reason: String) -> void:
terminated = true
terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial,
"stem": stem, "reason": reason, "suppressed_after_terminal": 0}
json_file(role + "-terminal.json", terminal_record)
expect(false, reason)
tree.quit(1)
func request(action: Dictionary) -> void:
if terminated:
return
serial += 1
var stem := "%s-request-%04d" % [role, serial]
var folder := directory.path_join(stem)
DirAccess.make_dir_recursive_absolute(folder)
var window: Window = main.get_window()
var view: WorldPreview = main.world_preview
action["report"] = Codec.new().typed(view.get_life_observation_report())
action["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
action["local_to_viewport"] = affine(view.get_global_transform_with_canvas())
action["local_to_screen"] = affine(view.get_screen_transform())
action["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
action["requested_ticks_usec"] = Time.get_ticks_usec()
action["requested_unix_ns"] = int(Time.get_unix_time_from_system() * 1000000000.0)
action["screenshot_receipt_schema"] = {"screenshots": [{"path": "evidence-root-relative raw image path", "raw_sha256": "original byte SHA256", "tool_call_id": "actual CUA screenshot call ID"}]}
expect(stream.capture(stem, runtime, view), "request state/report")
action.merge({"sequence": serial, "target_pid": OS.get_process_id(), "role": role,
"state": "waiting_for_root_cua", "window_title": window.title,
"window_position": [window.position.x, window.position.y],
"window_size": [window.size.x, window.size.y], "screen_scale": DisplayServer.screen_get_scale(window.current_screen),
"content_scale_factor": window.content_scale_factor, "setup": "diagnostic"})
json_file(stem + "/request.json", action)
var path := folder.path_join("ack.json")
var first_event: int = input_observer.events.size()
var deadline := Time.get_ticks_msec() + 300000
print("P23_ROOT_CUA_WAIT " + folder.path_join("request.json"))
while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline:
await tree.process_frame
if not FileAccess.file_exists(path):
terminate(stem, "Root CUA acknowledgement timeout")
return
var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path))
if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")):
terminate(stem, "Root CUA acknowledgement identity/hash")
return
await frames()
for event: Dictionary in input_observer.events.slice(first_event):
if event.has("dispatch"):
last_dispatch = event["dispatch"]
json_file(stem + "/observed.json", {"pid": OS.get_process_id(), "sequence": serial,
"ack_observed_ticks_usec": Time.get_ticks_usec(), "request_sha256": FileAccess.get_sha256(folder.path_join("request.json")),
"ack_observed_unix_ns": int(Time.get_unix_time_from_system() * 1000000000.0),
"observer_endpoint": "null_modal_receipts_only" if input_observer.endpoint == null else "passive_read_only_dispatch",
"events": input_observer.events.slice(first_event), "status": main.build_status_label.text})
receipts.append("os-pair/" + layout + "/" + stem + "/ack.json")
all_requests.append("os-pair/" + layout + "/" + stem + "/ack.json")
func annotate(row: Dictionary) -> void:
var view: WorldPreview = main.world_preview
row["rectangles"] = Codec.new().typed(draw_rectangles(view.get_life_observation_report(), view))
row["viewport_to_screen"] = affine(view.get_screen_transform() * view.get_global_transform_with_canvas().affine_inverse())
row["reveal"] = reveal_rows.duplicate(true)
reveal_rows.clear()
row["input_method"] = "native_os"
row["requests"] = receipts.duplicate()
receipts.clear()
# Rectangles describe existing draw commands; no second renderer is invoked.
func draw_rectangles(report: Dictionary, view: WorldPreview) -> Dictionary:
var rows: Array = []
for id: Variant in report.get(&"rooms", {}):
var room: Dictionary = report[&"rooms"][id]
if room.has(&"rect"):
rows.append({"category": "room", "id": id, "rect": room[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for index: int in range(report.get(&"landing_badges", []).size()):
var badge: Dictionary = report[&"landing_badges"][index]
rows.append(badge_rectangle("queue", str(index), badge[&"at"], badge[&"text"]))
for cabin: Dictionary in report.get(&"cabins", []):
rows.append({"category": "EV", "id": cabin[&"shaft_id"], "rect": cabin[&"rect"], "style_source": "scripts/ui/pixel_tower_art.gd"})
for actor: Dictionary in cabin[&"passengers"]:
rows.append(actor_rectangle(actor))
for actor: Dictionary in report.get(&"actor_drawings", []):
rows.append(actor_rectangle(actor))
if actor[&"staff"] and report[&"resting_staff"].has(StringName(String(actor[&"key"]).trim_prefix("staff/"))):
var rest := actor_rectangle(actor)
rest["category"] = "rest"
rows.append(rest)
for index: int in range(report.get(&"badges", []).size()):
var badge: Dictionary = report[&"badges"][index]
rows.append(badge_rectangle("goods" if badge[&"kind"] == &"supply" else "business", str(index), badge[&"at"], badge[&"text"]))
var transform := view.get_global_transform_with_canvas()
for row: Dictionary in rows:
row["viewport_rect"] = transform * (row["rect"] as Rect2)
row["screen_rect"] = view.get_screen_transform() * (row["rect"] as Rect2)
var source: String = FileAccess.get_file_as_string("res://" + row["style_source"])
var regex := RegEx.new()
regex.compile('Color\\("([0-9a-fA-F]{6})"')
var colors: Array = []
for found: RegExMatch in regex.search_all(source):
if not colors.has(found.get_string(1)): colors.append(found.get_string(1))
row["style_colors"] = colors
return {"rows": rows}
func badge_rectangle(category: String, id: String, at: Vector2, text: String) -> Dictionary:
var width := ThemeDB.fallback_font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, 10).x + 6
return {"category": category, "id": id, "rect": Rect2(at, Vector2(width, 14)),
"text": text, "font_width": width - 6, "style_source": "scenes/main/world_preview.gd"}
func actor_rectangle(actor: Dictionary) -> Dictionary:
return {"category": "staff" if actor[&"staff"] else "guest", "id": actor[&"key"],
"rect": Rect2(actor[&"at"], Vector2(16, 18) * float(actor[&"scale"])), "style_source": "scripts/ui/pixel_people.gd"}
func point(node: Control) -> Dictionary:
var viewport: Vector2 = node.get_global_transform_with_canvas() * (node.size * 0.5)
var screen: Vector2 = node.get_screen_transform() * (node.size * 0.5)
return {"node": String(node.get_path()), "viewport": [viewport.x, viewport.y], "screen": [screen.x, screen.y], "window_id": node.get_window().get_window_id(),
"viewport_to_screen": affine(node.get_screen_transform() * node.get_global_transform_with_canvas().affine_inverse())}
func setup() -> void:
await super.setup()
input_method = "native_os"
expect(DisplayServer.get_name() != "headless", "real OS display")
tree.root.title = "The Tree P23-03 " + layout + " " + role + " PID " + str(OS.get_process_id())
input_observer = InputObserver.new()
# No handler dispatch: this observer only records incoming OS events.
tree.root.add_child(input_observer)
json_file(role + "-profile.json", {"pid": OS.get_process_id(),
"user_data_dir": OS.get_user_data_dir(), "engine": Engine.get_version_info(),
"display": DisplayServer.get_name()})
func button(node: BaseButton) -> void:
expect(node != null and not node.disabled, "reachable ordinary button")
if node == null or node.disabled: return
await request({"action": "click", "point": point(node), "button_text": str(node.get("text"))})
func load_menu(menu: MenuButton) -> void:
var action := point(menu)
action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."})
await request(action)
func screenshot(label: String) -> void:
await frames()
await request({"action": "screenshot", "id": label, "layout": layout})
func layer_observations(prefix: String) -> void:
if role == "writer" and prefix == "writer": await screenshot("calibration")
await button(main.front_button)
await screenshot(prefix + "-FRONT")
await button(main.back_button)
await screenshot(prefix + "-BACK")
if role == "writer" and prefix == "writer": await ordinary_camera()
func ordinary_camera() -> void:
var view: WorldPreview = main.world_preview
var at := point(view)
expect(not view.pan_mode, "ordinary zoom starts with move mode off")
var zoom: float = view.zoom_level
await request({"action": "wheel", "point": at, "direction": "down",
"ctrl": false, "move_mode": false})
expect(view.zoom_level != zoom, "ordinary wheel changes zoom")
await screenshot("ordinary-wheel-zoom")
await button(main.pan_button)
await screenshot("pan-mode-button")
expect(view.pan_mode, "PanModeButton enables move mode")
zoom = view.zoom_level
var pan: Vector2 = view.pan_offset
await request({"action": "wheel", "point": point(view), "direction": "up",
"ctrl": false, "move_mode": true})
expect(view.pan_offset != pan and view.zoom_level == zoom, "ordinary move-mode wheel pans")
await screenshot("move-mode-wheel-pan")
func finish_observations() -> void:
json_file(role + "-requests.json", {"requests": all_requests, "pid": OS.get_process_id()})
if is_instance_valid(input_observer): input_observer.queue_free()
super.finish_observations()
+8
View File
@@ -0,0 +1,8 @@
extends SceneTree
const Passive := preload("res://tests/fixtures/p23-03/passive_observation.gd")
func _initialize() -> void:
call_deferred("run")
func run() -> void:
var result: Dictionary = await Passive.new().run(self, OS.get_environment("P23_OBSERVATION_ROOT"), OS.get_environment("P23_OBSERVATION_ROLE"))
print("P23 passive OS observation: %d checks, %d failures" % [result.checks, result.failures])
quit(0 if result.failures == 0 else 1)
+8
View File
@@ -15,6 +15,14 @@ func expect(value: bool, message: String) -> void:
push_error("P23 life observation: " + message)
func run() -> void:
var layout := OS.get_environment("P23_LAYOUT")
var output_directory := OS.get_environment("P23_OUTPUT")
if layout in ["A", "B"] and not output_directory.is_empty():
var comparison := preload("res://tests/fixtures/p23-03/layout_comparison.gd").new()
expect(await comparison.run(self, output_directory, layout), "native layout " + layout)
print("P23 life observation: %d checks, %d failures" % [checks, failures])
quit(0 if failures == 0 else 1)
return
var clock: Variant = root.get_node("GameClock")
clock.set_paused(true)
# Isolated real typed components test state labels, without claiming gameplay.
+828 -11
View File
@@ -10,9 +10,12 @@ import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import re
import sys
import struct
import zlib
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parent.parent
@@ -24,15 +27,13 @@ CONDITIONS = (
'supplies_and_carried_goods', 'same_demand_route_improvement',
'normal_os_and_distinct_pid_resume',
)
BLOCKERS = {
CONDITIONS[0]: 'Service/rescue occupancy, reason fields and room rectangles checked; rest route selection and badge pixels unfinished.',
CONDITIONS[1]: 'Queue lane allocation, overflow and positions checked; screenshot pixel provenance unfinished.',
CONDITIONS[2]: 'Shaft/trip and cabin geometry checked; prior-frame interpolation history and passenger geometry unfinished.',
CONDITIONS[3]: 'Typed staff/task projection implemented; motion/rest/rescue/cleaning drawing recomputation unfinished.',
CONDITIONS[4]: 'Carried inventory ID and logistics report checked; supply badge pixels unfinished.',
CONDITIONS[5]: 'Public-command A/B fixture and typed arrival metric added; native execution, definition pins and full command/path provenance checks pending.',
CONDITIONS[6]: 'P23 passive request/ACK collector and independent CUA/formal-resume gates unfinished; Root evidence missing.',
}
CATEGORIES = ('room', 'queue', 'EV', 'staff', 'goods', 'business')
CONDITION_CHECKS = dict(zip(CONDITIONS, ('room_users', 'queue_overflow', 'elevator_passengers',
'staff_jobs', 'carried_goods', 'route_improvement', 'distinct_pid_resume')))
# Required names, not verdicts. BLOCKERS is computed from actual execution below.
REQUIRED_CHECKS = {condition: {category: {'semantic_' + category, 'pixels_' + category,
CONDITION_CHECKS[condition], 'os_guards', 'resume_recalculation'}
for category in CATEGORIES} for condition in CONDITIONS}
def module(name, path):
@@ -74,6 +75,199 @@ def sha(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def png_pixels(data):
"""Decode noninterlaced 8-bit RGB/RGBA PNG, validating the whole stream."""
require(data[:8] == b'\x89PNG\r\n\x1a\n', 'PNG signature')
pos, header, payload, ended, idat_closed = 8, None, bytearray(), False, False
while pos < len(data):
require(pos + 12 <= len(data), 'PNG truncated chunk')
size, kind = struct.unpack('>I4s', data[pos:pos + 8])
require(pos + size + 12 <= len(data), 'PNG truncated payload')
body = data[pos + 8:pos + 8 + size]
crc = struct.unpack('>I', data[pos + 8 + size:pos + 12 + size])[0]
require(zlib.crc32(kind + body) == crc, 'PNG CRC mismatch')
pos += size + 12
require(header is not None or kind == b'IHDR', 'PNG IHDR must be first')
if kind == b'IHDR':
require(header is None and size == 13, 'PNG duplicate/invalid IHDR')
w, h, depth, color, compression, filtering, interlace = struct.unpack('>IIBBBBB', body)
require(w > 0 and h > 0 and depth == 8 and color in (2, 6)
and (compression, filtering, interlace) == (0, 0, 0), 'PNG unsupported IHDR')
header = (w, h, 3 if color == 2 else 4)
elif kind == b'IDAT':
require(not idat_closed, 'PNG nonconsecutive IDAT')
payload.extend(body)
elif kind == b'IEND':
require(size == 0 and payload and pos == len(data), 'PNG invalid IEND')
ended = True
break
else:
require(kind[0] & 32 or kind == b'PLTE', 'PNG unknown critical chunk')
if payload:
idat_closed = True
require(ended and header is not None, 'PNG missing IEND')
w, h, channels = header
stride = w * channels
expected = h * (stride + 1)
try:
decoder = zlib.decompressobj()
raw = decoder.decompress(bytes(payload), expected + 1)
require(decoder.eof and not decoder.unused_data and not decoder.unconsumed_tail
and len(raw) == expected, 'PNG zlib length/end mismatch')
except zlib.error as exc:
raise ValueError('PNG invalid zlib') from exc
rows, prior = [], bytearray(stride)
for y in range(h):
start = y * (stride + 1)
mode = raw[start]
require(mode in range(5), 'PNG unknown filter')
row = bytearray(raw[start + 1:start + 1 + stride])
for x in range(stride):
left = row[x - channels] if x >= channels else 0
up = prior[x]
corner = prior[x - channels] if x >= channels else 0
if mode == 1:
predictor = left
elif mode == 2:
predictor = up
elif mode == 3:
predictor = (left + up) // 2
elif mode == 4:
p = left + up - corner
distances = (abs(p - left), abs(p - up), abs(p - corner))
predictor = (left, up, corner)[distances.index(min(distances))]
else:
predictor = 0
row[x] = (row[x] + predictor) & 255
rows.append([tuple(row[x:x + channels]) for x in range(0, stride, channels)])
prior = row
return w, h, rows
def image_kind(data):
if data.startswith(b'\x89PNG\r\n\x1a\n'):
w, h, _ = png_pixels(data)
return 'png', w, h
if data.startswith(b'\xff\xd8'):
helper = module('p23_p08_jpeg', ROOT / 'tools/check_p08_normal_placement_evidence.py')
w, h = helper.jpeg_dimensions(data)
return 'jpeg', w, h
raise ValueError('unknown screenshot byte magic')
def inspect_conversion(rec, cua_receipt, converter_sha256=None):
"""CUA receipt and converter pin are independent of conversion assertions."""
raw, png = Path(rec['raw_path']), Path(rec['png_path'])
require(raw.resolve() != png.resolve() and not raw.is_symlink() and not png.is_symlink(),
'conversion must preserve a separate raw file')
require(rec['raw_sha256'] == sha(raw) == cua_receipt['raw_sha256']
and rec['tool_call_id'] == cua_receipt['tool_call_id']
and bool(rec['tool_call_id']), 'CUA raw hash/call mismatch')
require(rec['raw_kind'] == 'jpeg', 'conversion raw kind')
kind, w, h = image_kind(raw.read_bytes())
require(kind == 'jpeg', 'conversion requires JPEG magic')
require(rec['converter'] == '/usr/bin/sips', 'conversion executable')
pin = converter_sha256 if converter_sha256 is not None else sha('/usr/bin/sips')
require(rec['converter_sha256'] == pin, 'converter SHA mismatch')
require(rec['argv'] == ['/usr/bin/sips', '-s', 'format', 'png', str(raw), '--out', str(png)],
'converter argv mismatch')
require(type(rec['exit_code']) is int and rec['exit_code'] == 0, 'converter exit code')
for key in ('stdout_sha256', 'stderr_sha256'):
require(isinstance(rec[key], str) and re.fullmatch('[0-9a-f]{64}', rec[key]),
'invalid converter output digest')
require(sha(png) == rec['png_sha256'], 'converted PNG SHA mismatch')
pw, ph, rows = png_pixels(png.read_bytes())
require((w, h) == (pw, ph) == (rec['width'], rec['height']), 'conversion dimensions mismatch')
return pw, ph, rows
def screenshot_pixels(path, conversion=None, cua_receipt=None, converter_sha256=None):
data = Path(path).read_bytes()
kind, _, _ = image_kind(data)
if kind == 'jpeg':
require(conversion is not None, 'JPEG screenshot requires conversion record')
require(Path(conversion['raw_path']).resolve() == Path(path).resolve(), 'conversion raw path mismatch')
require(cua_receipt is not None, 'missing CUA receipt')
return inspect_conversion(conversion, cua_receipt, converter_sha256)
return png_pixels(data)
def affine_point(matrix, point):
require(len(matrix) == 6 and all(type(v) in (int, float) and math.isfinite(v) for v in matrix), 'invalid affine')
a, b, c, d, x, y = matrix
require(a*d-b*c != 0, 'singular affine')
return [a*point[0]+c*point[1]+x, b*point[0]+d*point[1]+y]
def affine_rect(matrix, rect):
x, y, w, h = rect
points = [affine_point(matrix, p) for p in ([x,y], [x+w,y], [x,y+h], [x+w,y+h])]
lo = [min(p[i] for p in points) for i in (0, 1)]
return lo + [max(p[i] for p in points)-lo[i] for i in (0, 1)]
def compare_rectangles(report, audit, local_to_viewport, viewport_to_screen, local_to_screen, image, root=ROOT):
"""All draw rows, full affine composition, and exact existing style colors.
Font advance is recorded by the pinned engine/font, not approximated using
a new text width threshold. No image similarity threshold is introduced.
"""
for point in ([0, 0], [1, 0], [0, 1]):
close(affine_point(viewport_to_screen, affine_point(local_to_viewport, point)),
affine_point(local_to_screen, point), 'viewport/screen composition')
rows = audit['rows']
expected = []
tower, people, world = 'scripts/ui/pixel_tower_art.gd', 'scripts/ui/pixel_people.gd', 'scenes/main/world_preview.gd'
def actor(a, category=None):
expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'],
a['at'] + [16*a['scale'], 18*a['scale']], people, None))
def badge(category, identifier, at, text):
expected.append((category, identifier, at + [None, 14], world, text))
for fid, room in report['rooms'].items():
if 'rect' in room:
expected.append(('room', fid, room['rect'], tower, None))
for i, b in enumerate(report['landing_badges']):
badge('queue', str(i), b['at'], b['text'])
for cabin in report['cabins']:
expected.append(('EV', cabin['shaft_id'], cabin['rect'], tower, None))
for a in cabin['passengers']:
actor(a)
for a in report['actor_drawings']:
actor(a)
if a['staff'] and a['key'].removeprefix('staff/') in report['resting_staff']:
actor(a, 'rest')
for i, b in enumerate(report['badges']):
badge('goods' if b['kind'] == 'supply' else 'business', str(i), b['at'], b['text'])
require(len(rows) == len(expected), 'rectangle coverage')
width, height, pixels = image
checked = []
for row, (category, identifier, rect, source, text) in zip(rows, expected):
require((row['category'], row['id'], row['style_source']) == (category, identifier, source), 'rectangle row identity')
if text is not None:
require(row['text'] == text and type(row['font_width']) in (int, float) and row['font_width'] > 0, 'badge font/text')
rect[2] = row['font_width'] + 6
close(row['rect'], rect, 'local drawing rectangle')
viewport = affine_rect(local_to_viewport, rect)
screen = affine_rect(viewport_to_screen, viewport)
close(row['viewport_rect'], viewport, 'viewport rectangle')
close(row['screen_rect'], screen, 'screen rectangle')
colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text())))
require(row['style_colors'] == colors and colors, 'drawing style palette')
x, y, w, h = screen
require(w > 0 and h > 0 and x >= 0 and y >= 0 and x+w <= width and y+h <= height,
'drawing rectangle outside screenshot')
# Badge ink is distinct from its translucent backing. Actor ink/skin
# and facility colors are taken verbatim from their pinned draw source.
palette = {'fff2ce'} if text is not None else set(colors)
palette -= {'303c40', 'c6d3b6', 'd5dec4'} # shadow / sky decorations
rgb = {tuple(bytes.fromhex(c)) for c in palette}
found = any(tuple(pixels[py][px][:3]) in rgb
for py in range(math.ceil(y), math.floor(y+h))
for px in range(math.ceil(x), math.floor(x+w)))
require(found, 'missing drawing pixels: ' + category + '/' + str(identifier))
checked.append(category + '/' + str(identifier))
return checked
def enum_names(root, relative, name):
source = (root / relative).read_text()
match = re.search(r'\benum\s+' + re.escape(name) + r'\s*\{([^}]+)\}', source)
@@ -322,6 +516,117 @@ def comparison_records(directory):
return records
def room_slots(rect, pixel, definition='staff_waiting_room'):
x, y, w, h = rect
step = 20 if definition in ('cafeteria', 'tavern') else 16
columns = max(int(w / pixel / step), 1)
rows = max(int((h / pixel - (30 if definition == 'chapel' else 10)) / 20), 1)
if definition == 'chapel':
columns = min(columns, 4)
anchors = [[x + (col + .5) * w / columns, y + h - (4 + row * 20) * pixel]
for row in range(rows) for col in range(columns)]
return anchors[:8] if definition == 'chapel' else anchors
def compare_rest(state, report, audit, root=ROOT):
"""Recompute selection, not shortest paths. Shortest-path authority is the
exact pinned PathService source producing the recorded public API paths.
Edge reports must be nonempty, including for temporarily closed cells.
"""
require(audit['path_service_sha256'] == sha(root / 'scripts/world/path_service.gd'),
'rest PathService source pin')
runtime = state['systems']['runtime']
members = runtime['staff']['staff']
facilities = runtime['facilities']['instances']
require([r['staff_id'] for r in audit['staff']] == [r['staff_id'] for r in members],
'rest staff order/coverage')
passable = {tuple(r['cell']) for r in runtime['path']['cell_traffic']}
resting = {}
selections = {}
for member, row in zip(members, audit['staff']):
sid = member['staff_id']
for key in ('state', 'health', 'current_task_id', 'current_cell'):
close(row[key], member[key], 'rest input/' + key)
memberships = [dict(raid_id=raid['raid_id'], state=raid['state'], side=c['side'])
for raid in runtime['defense']['raids'] for c in raid['combatants']
if c['actor_id'] == sid]
close(row['defense_memberships'], memberships, 'rest defense membership')
locked = sid in runtime['events']['actor_locks']
transit = any(t['actor_id'] == 'staff:' + sid for t in runtime['elevators']['trips'])
close(row['actor_lock'], locked, 'rest actor lock')
close(row['in_transit'], transit, 'rest transit')
defense = any(r['state'] not in ('DEFENDED', 'BREACHED', 'DEFENDERS_RETREATED', 'CANCELLED')
and r['side'] == 'DEFENDER' for r in memberships)
eligible = member['state'] == 'OFF_DUTY' and member['health'] > 0 and not member['current_task_id'] and not locked and not defense
selected = dict(reachable=False, resting=False, label='休憩先なし(稼働中の待機室・宿舎と経路を確認)') if eligible else {}
best_cost = 2147483647
require([f['instance_id'] for f in row['facilities']] == [f['instance_id'] for f in facilities],
'rest facility order/coverage')
for facility, observed in zip(facilities, row['facilities']):
for key in ('instance_id', 'definition_id', 'origin', 'occupied_cells'):
close(observed[key], facility[key], 'rest facility/' + key)
operation = 'SUSPENDED' if facility['suspension_reason'] else ('OPERATING' if facility['state'] in ('OPEN', 'DEGRADED') else 'INACTIVE')
close(observed['operation_state'], operation, 'rest operation')
occupied = facility['occupied_cells'] or [facility['origin']]
probes = [dict(cell=[c[0] + dx, c[1], c[2]], has_cell=(c[0] + dx, c[1], c[2]) in passable)
for c in occupied for dx in (0, -1, 1)]
close(observed['probes'], probes, 'rest candidate probes')
ordered = sorted({tuple(p['cell']) for p in probes if p['has_cell']}, key=lambda c: (c[2], c[1], c[0]))
close(observed['ordered'], [list(c) for c in ordered], 'rest candidate order')
require([r['cell'] for r in observed['routes']] == [list(c) for c in ordered], 'rest route coverage/order')
best = None
for route in observed['routes']:
path, edges = route['path'], route['edges']
require(len(edges) == max(len(path) - 1, 0), 'rest edge coverage')
if not path:
require(route['calculate_path_cost'] == -1, 'rest empty route cost')
continue
require(path[0] == member['current_cell'] and path[-1] == route['cell'], 'rest path endpoints')
require(all(tuple(c) in passable for c in path), 'rest path cell absent')
cost = 0
for a, b, edge in zip(path, path[1:], edges):
require(edge and edge['from'] == a and edge['to'] == b, 'rest empty or discontinuous edge report')
require(type(edge['total_cost']) is int and edge['total_cost'] >= 0, 'rest edge cost')
cost += edge['total_cost']
require(cost == route['calculate_path_cost'], 'rest edge sum differs from calculate_path_cost')
if best is None or cost < best[0]:
best = (cost, path[-1])
if not eligible or operation != 'OPERATING' or facility['definition_id'] not in ('staff_waiting_room', 'staff_dormitory') or best is None or best[0] >= best_cost:
continue
best_cost = best[0]
cell = member['current_cell']
at = any(c[2] == cell[2] and c[1] == cell[1] and abs(c[0] - cell[0]) <= 1 for c in occupied) and not transit
selected = dict(reachable=True, resting=at, cell=best[1], facility_id=facility['instance_id'],
label=('宿舎で休憩中' if facility['definition_id'] == 'staff_dormitory' else '待機室で休憩中') if at else '休憩先へ移動中')
close(row['comparison_only'], selected, 'rest selection comparison')
selections[sid] = selected
if selected.get('resting'):
resting[sid] = selected['facility_id']
close(report['resting_staff'], resting, 'draw resting_staff')
transform = Transform(runtime, report)
drawings = {r['key']: r for r in report['actor_drawings']}
for sid, fid in resting.items():
member = next(m for m in members if m['staff_id'] == sid)
if member['current_cell'][2] != report['layer'] or sid in report['carried_patients']:
continue
room = report['rooms'].get(fid, {})
cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), [])
if not cells:
continue # renderer uses its ordinary tracked fallback
require(sid in room.get('people', []), 'rest room occupant missing')
slot = room['people'].index(sid)
anchors = room_slots(transform.room(cells), transform.dim[0] / 16)
key = 'staff/' + sid
if key in report['boarded'] or key in report['queue_hidden']:
require(key not in drawings, 'hidden resting drawing')
elif slot >= len(anchors):
require(key not in drawings, 'rest slot overflow drawing')
else:
close(drawings[key]['center'], [anchors[slot][0], anchors[slot][1] - transform.dim[1] * .5],
'resting staff position')
return selections
def compare_goods(snapshot, report, root=ROOT):
runtime = snapshot['systems']['runtime']
states = enum_names(root, 'scripts/inventory/logistics_request.gd', 'State')
@@ -356,6 +661,42 @@ def compare_goods(snapshot, report, root=ROOT):
close(report['goods'], dict(requests=rows, deliveries=active + [r[2] for r in recent[:10]]), 'goods')
def compare_badges(state, report):
runtime = state['systems']['runtime']
transform = Transform(runtime, report)
expected = []
labels = dict(BROKEN='破損', NO_ACCESS='通路なし', NO_POWER='魔力なし', NO_STAFF='職員不足',
CLOSED='営業停止', DEGRADED='低下運転', OPEN='営業中')
for facility in runtime['facilities']['instances']:
fid = facility['instance_id']
if facility['origin'][2] != report['layer']: continue
cells = next((r['cells'] for r in runtime['world']['occupancies'] if r['entity_id'] == fid), [])
if not cells: continue
x,y,w,h = transform.room(cells)
room = report['rooms'][fid]
count, limit = room['count'], facility['customer_capacity']
if facility['suspension_reason']:
business = '休止・摘発' if facility['suspension_reason'] == 'RAIDED' else '休止・破壊'
label = business
else:
business = labels.get(facility['state'], facility['state'])
if facility['state'] not in ('OPEN', 'DEGRADED'):
label = '未稼働'
else:
label = f'利用{count}/{limit}人' if limit >= 0 else f'利用{count}人'
if limit > 0 and count >= limit: label = '満員 ' + label
visible = len(room_slots([x,y,w,h], transform.dim[0]/16, facility['definition_id']))
if count > visible: label += f'(他{count-visible}人)'
expected.append(dict(facility=fid, kind='business', at=[x+2,y+2], text=business+' '+label))
requests = [r for r in report['goods']['requests'] if fid in (r['source_facility_id'], r['destination_facility_id'])]
if requests:
states = {}
for r in requests: states[r['state']] = states.get(r['state'], 0)+1
text = '物資依頼 ' + ' / '.join(f'{k}:{states[k]}' for k in sorted(states))
expected.append(dict(facility=fid, kind='supply', at=[x+2,y+16], requests=requests, text=text))
close(report['badges'], expected, 'business/supply badges')
def compare_cabin_geometry(snapshot, report):
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
@@ -394,6 +735,173 @@ def compare_cabin_geometry(snapshot, report):
close(row[key], value, 'cabin/' + key)
def compare_motion(state, report, audit):
frames = audit['frames']
require(len(frames) == 2, 'two motion history frames required')
previous, current = frames
require(type(previous['frame']) is int and current['frame'] > previous['frame'], 'motion frame order')
require(current['delta'] >= 0 and current['scaled_delta'] >= 0, 'motion delta')
before, after = previous['display'], current['display']
for key, value in after.items():
close(report[key], value, 'motion report binding/' + key)
close(after['animation_time'], before['animation_time'] + current['scaled_delta'], 'motion animation delta')
if before['paused'] and after['paused']:
close(current['scaled_delta'], 0, 'paused animation moved')
now = after['animation_time']
runtime = state['systems']['runtime']
cells = {'guest/' + r['guest_id']: r['current_cell'] for r in runtime['guests']['guests']}
cells.update({'staff/' + r['staff_id']: r['current_cell'] for r in runtime['staff']['staff']})
def interpolated(track, endpoint, paused, transform=None):
t = 1 if paused else max(0, min(1, (now-track['at'])/.42))
if transform:
a, b = transform.center(track['from']), transform.center(endpoint)
return [a[i] + (b[i]-a[i])*t for i in (0, 1)]
return track['from']+(endpoint-track['from'])*t
for key, track in after['actor_tracks'].items():
require(key in cells and track['cell'] == cells[key], 'motion actor endpoint')
old = before['actor_tracks'].get(key)
if old is None:
wanted = dict(cell=cells[key], **{'from': cells[key]}, at=now, direction=1.0)
elif old['cell'] == cells[key]:
wanted = old
else:
a, b = old['cell'], cells[key]
origin = a if a[2] == b[2] and abs(a[0]-b[0])+abs(a[1]-b[1]) == 1 else b
wanted = dict(cell=b, **{'from': origin}, at=now,
direction=(1.0 if b[0] > a[0] else -1.0) if a[0] != b[0] else old['direction'])
close(track, wanted, 'actor interpolation origin/history')
# Ordinary walking centers; queue/room/rescue/working projections have
# independent semantic placement checks and do not use this center.
drawing = next((a for a in after['actor_drawings'] if a['key'] == key), None)
if drawing and drawing['mode'] in ('walk', 'carry_walk') and key not in report['queue_positions']:
position = interpolated(track, cells[key], after['paused'], Transform(runtime, report))
# Same-cell crowd offsets are handled by the complete actor check.
require(all(math.isfinite(v) for v in position), 'nonfinite interpolation')
floors = {s['shaft_id']: s['visual_floor'] for s in elevator_report(state)['shafts']}
for key, track in after['elevator_tracks'].items():
require(key in floors, 'unknown elevator motion')
old = before['elevator_tracks'].get(key)
if old is None:
wanted = dict(floor=floors[key], **{'from': floors[key]}, at=now)
elif math.isclose(old['floor'], floors[key], abs_tol=.00001, rel_tol=.00001):
wanted = old
else:
wanted = dict(floor=floors[key], **{'from': old['floor']}, at=now)
close(track, wanted, 'elevator interpolation origin/history')
if before['paused'] and after['paused'] and all(before[k] == after[k] for k in ('layer', 'zoom', 'pan', 'viewport', 'actor_tracks', 'elevator_tracks')):
prior_actors = {r['key']: r for r in before['actor_drawings']}
for row in after['actor_drawings']:
old = prior_actors.get(row['key'])
if old and all(old[k] == row[k] for k in ('state', 'mode', 'activity', 'receiving', 'carrying')):
close(row['at'], old['at'], 'paused actor moved')
return ['motion_history', 'interpolation', 'pause']
def compare_actor_geometry(state, report):
runtime = state['systems']['runtime']
transform = Transform(runtime, report)
p, height = transform.dim[0]/16, transform.dim[1]
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
staff = codec.indexed(runtime['staff']['staff'], 'staff_id')
occupancy = {r['entity_id']: r['cells'] for r in runtime['world']['occupancies']}
counts, expected = {}, []
def track_position(key, cell):
track = report['actor_tracks'][key]
close(track['cell'], cell, 'actor authoritative cell')
t = 1 if report['paused'] else max(0, min(1, (report['animation_time']-track['at'])/.42))
a, b = transform.center(track['from']), transform.center(cell)
return [a[i]+(b[i]-a[i])*t for i in (0, 1)]
def offset(cell, center):
key = tuple(cell)
n = counts.get(key, 0)
counts[key] = n+1
return [center[0]+[0,4,-4,2,-2,6,-6][n%7]*p, center[1]-(n%2)*p]
def slot_position(fid, identifier, definition, fallback, dy, missing_fallback=False):
cells = occupancy.get(fid, [])
if not cells:
return fallback
users = report['rooms'].get(fid, {}).get('people', [])
if identifier not in users:
return fallback if missing_fallback else None
anchors = room_slots(transform.room(cells), p, definition)
index = users.index(identifier)
return [anchors[index][0], anchors[index][1]+dy] if index < len(anchors) else None
for guest in runtime['guests']['guests']:
key, cell = 'guest/'+guest['guest_id'], guest['current_cell']
if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']:
continue
center = track_position(key, cell)
if key in report['queue_positions']:
center = report['queue_positions'][key]
elif guest['current_service_id']:
fid = guest['target_facility_id']
definition = facilities.get(fid, {}).get('definition_id', '')
cells = occupancy.get(fid, [])
if cells and cells[0][2] == report['layer']:
center = slot_position(fid, guest['guest_id'], definition, center,
3*p-height*.5-(5*p if definition in ('standard_room', 'premium_room') else 0), True)
else:
center = offset(cell, center)
if center is not None:
expected.append((key, center, False, bool(guest['current_service_id']), False, False))
for sid, member in staff.items():
key, cell = 'staff/'+sid, member['current_cell']
if key in report['boarded'] or key in report['queue_hidden'] or cell[2] != report['layer']:
continue
center = track_position(key, cell)
special = True
if sid in report['carried_patients']:
carrier = staff.get(report['carried_patients'][sid])
if carrier:
center = track_position('staff/'+carrier['staff_id'], carrier['current_cell'])
center[1] -= 12*p
elif sid in report['resting_staff']:
center = slot_position(report['resting_staff'][sid], sid, 'staff_waiting_room', center, -height*.5)
elif sid in report['treated_patients']:
center = slot_position(report['treated_patients'][sid], sid, 'infirmary', center, -2*p-height*.5)
else:
special = False
center = offset(cell, center)
fid = member['assigned_facility_id']
cells = occupancy.get(fid, [])
workers = report['rooms'].get(fid, {}).get('staff', [])
if member['state'] in ('WORKING', 'AVAILABLE') and fid in facilities and cell in cells and sid in workers:
rect = transform.room(cells)
columns = max(int(rect[2]/(16*p)), 1)
index = workers.index(sid)
if rect[3] >= 40*p and index < columns:
center = [rect[0]+(index+.5)*rect[2]/columns, rect[1]+26*p-height*.5]
center = report['queue_positions'].get(key, center)
if center is not None:
expected.append((key, center, True, False, bool(member['carried_inventory_id']) and not special, False))
require([r['key'] for r in report['actor_drawings']] == [r[0] for r in expected], 'actor drawing coverage/order')
pairs = list(zip(report['actor_drawings'], expected))
shafts = {s['shaft_id']: s for s in elevator_report(state)['shafts']}
for cabin in report['cabins']:
shaft = shafts[cabin['shaft_id']]
x,y,w,h = cabin['rect']
columns = max(int((w/p-4)/8), 1)
passengers = shaft['boarded'][:columns*2]
require(len(cabin['passengers']) == len(passengers), 'passenger coverage')
for i, (actor_id, row) in enumerate(zip(passengers, cabin['passengers'])):
key = actor_id.replace('staff:', 'staff/', 1).replace('guest:', 'guest/', 1)
is_staff = key.startswith('staff/')
center = [x+(2+(i%columns+.5)*8)*p, y+h-(2+(i//columns)*9)*p+3*p-height*.5]
carried = bool(staff.get(key.removeprefix('staff/'), {}).get('carried_inventory_id')) if is_staff else False
pairs.append((row, (key, center, is_staff, False, carried, True)))
for row, (key, center, is_staff, receiving, carrying, riding) in pairs:
close(row['center'], center, 'actor center/interpolation')
scale = .5 if riding else .75
scale = max(math.floor(p*scale), 1) if p >= 2 else p*scale
close(row['scale'], scale, 'actor scale')
at = [center[0]-8*scale, center[1]+height*.5-3*p-17*scale]
rounded = [math.copysign(math.floor(abs(v)+.5), v) for v in at]
close(row['at'], rounded, 'actor drawing position')
require((row['key'], row['staff'], row['receiving'], row['carrying'], row['riding']) ==
(key, is_staff, receiving, carrying, riding), 'actor drawing attributes')
return ['actor_geometry', 'passenger_geometry']
def compare_layouts(a, b):
require(a['declaration'] == b['declaration'], 'identical predeclared inputs')
declaration = a['declaration']
@@ -452,30 +960,339 @@ def inspect_stream(directory, root=ROOT):
labels.add(row['label'])
require(type(row['pid']) is int and row['pid'] > 0, 'actual diagnostic PID required')
state = read_object(directory, row['state'])
close(read_object(directory, row['state_after']), state, 'read-only audit snapshot identity')
report = read_object(directory, row['report'])
fields = compare_actor_rows(state, report, root)
require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch')
compare_room_services(state, report, root)
compare_rest(state, report, read_object(directory, row['rest_audit']), root)
compare_queues(state, report)
compare_traffic(state, report)
compare_goods(state, report, root)
compare_badges(state, report)
compare_cabin_geometry(state, report)
compare_motion(state, report, read_object(directory, row['motion']))
compare_actor_geometry(state, report)
checked.append(dict(label=row['label'], fields=fields + ['elevators', 'room_service_fields',
'queues', 'traffic', 'goods', 'cabin_geometry']))
require(checked, 'empty diagnostic stream')
return checked
def condition_matrix():
return [dict(condition=name, status='blocked', reason=BLOCKERS[name]) for name in CONDITIONS]
def condition_matrix(executed=None):
executed = executed or {}
result = []
for condition in CONDITIONS:
categories = {}
for category in CATEGORIES:
actual = set(executed.get(condition, {}).get(category, []))
required = REQUIRED_CHECKS[condition][category]
missing = sorted(required - actual)
categories[category] = dict(checks=sorted(actual), missing=missing,
status='passed' if actual and not missing else 'pending')
result.append(dict(condition=condition, categories=categories,
status='passed' if all(c['status'] == 'passed' for c in categories.values()) else 'pending'))
return result
def BLOCKERS(executed=None):
return {r['condition']: {c: d['missing'] for c, d in r['categories'].items() if d['missing']}
for r in condition_matrix(executed) if r['status'] != 'passed'}
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
PLAN = 'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md'
def normative_plan_text(root):
"""Requirement text is an input; explicitly labelled progress is metadata."""
text = (root / PLAN).read_text()
section = re.search(r'^### P23[^\n]*\n(.*?)(?=^### |\Z)', text, re.M | re.S)
require(section is not None, 'missing normative #p23 plan section')
lines = []
for line in section.group(0).splitlines():
if line.startswith('現在地:'):
continue
line = line.split(' 状態:', 1)[0].rstrip()
if line:
lines.append(line)
require(any(line.startswith('- P23-03 ') and '受入:' in line for line in lines),
'missing P23-03 work/acceptance plan text')
return '\n'.join(lines) + '\n'
def input_map(root):
names = [SPEC, CONTRACT, 'AGENTS.md', 'docs/traceability/original/BASELINE.txt']
names += [p.relative_to(root).as_posix() for p in (root / 'tests/fixtures/p23-03').rglob('*')
if p.is_file()]
pins = {name: sha(root / name) for name in sorted(names)}
pins[PLAN + '#p23-normative'] = hashlib.sha256(normative_plan_text(root).encode()).hexdigest()
return pins
def input_provenance(root):
# Full-plan progress changes deliberately do not participate in input equality.
return {'full_plan_sha256': sha(root / PLAN)}
def inspect_os_receipt(receipt, source_map, pins=None):
require(receipt.get('engine_ended') is True, 'engine_ended must be true')
require(receipt.get('passed') is True and receipt.get('exit_code') == 0, 'unsuccessful OS process')
require(not receipt.get('error') and receipt.get('guard_errors') == []
and receipt.get('cleanup_errors') == [], 'OS cleanup/guard failure')
require(receipt.get('default_before') == receipt.get('default_after')
and isinstance(receipt.get('default_before'), dict), 'default profile changed')
require(receipt.get('stage_before') == receipt.get('stage_after')
and isinstance(receipt.get('stage_before'), dict), 'stage changed')
require(receipt.get('source_map') == source_map, 'OS source pin differs')
after = receipt.get('source_after', {})
require({name: pin['sha256'] for name, pin in after.items()} == source_map, 'source changed')
cleanup = receipt.get('owned_cleanup')
require(isinstance(cleanup, list) and cleanup
and all(row.get('engine_ended') is True for row in cleanup), 'missing owned cleanup')
require(bool(receipt.get('ordinary_owner_proof')), 'missing ordinary audio owner proof')
for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'):
require(isinstance(receipt.get(field), str) and re.fullmatch('[0-9a-f]{64}', receipt[field])
and receipt[field] == receipt.get(field + '_after'), 'OS pin changed: ' + field)
require(receipt.get('spec_sha256') == SPEC_SHA, 'OS immutable spec')
require(receipt.get('inputs') and receipt['inputs'] == receipt.get('inputs_after'), 'OS inputs changed')
if pins is not None:
require(receipt['inputs'] == pins, 'OS input applicability')
require(receipt.get('environment') and receipt['environment'] == receipt.get('environment_after'), 'OS environment changed')
require(type(receipt.get('actual_pid')) is int and receipt['actual_pid'] > 0, 'OS actual PID')
require(receipt.get('profile') and receipt.get('profile_name') and isinstance(receipt.get('profile_after'), dict), 'OS private profile provenance')
before = receipt.get('profile_before', {})
require(type(before.get('existed')) is bool and isinstance(before.get('files'), dict), 'OS profile before')
for member, pin in before['files'].items():
require(receipt['profile_after'].get(member) == pin, 'OS profile input changed: ' + member)
def inspect_resume(writer, reader, saved, requests, formal_members):
require(type(writer['actual_pid']) is int and type(reader['actual_pid']) is int
and writer['actual_pid'] > 0 and reader['actual_pid'] > 0
and writer['actual_pid'] != reader['actual_pid'], 'resume requires distinct actual PIDs')
require(saved['pid'] == writer['actual_pid'], 'formal writer PID')
require(set(formal_members) == {'world.bin', 'manifest.json', 'thumbnail.png', 'journal.log'}, 'formal members coverage')
close(saved['members'], formal_members, 'formal member hashes')
require({k: v['sha256'] for k, v in reader['formal_input'].items()} == formal_members, 'reader formal input hashes')
loads = [row for row in requests if row['request']['action'] == 'load_menu']
require(len(loads) == 1, 'missing or duplicate normal load ACK')
item = loads[0]
request, ack, observed, cua = [item[k] for k in ('request', 'ack', 'observed', 'cua')]
require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == reader['actual_pid'], 'load ACK PID')
require(request['sequence'] == ack['sequence'] == observed['sequence'], 'load ACK request ID')
require(ack['request_sha256'] == item['request_sha256'] == observed['request_sha256'], 'load ACK hash')
require(0 < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'load ACK time order')
require(cua['method'] == 'native_os' and cua['tool_call_id'] and cua['timestamp_ns'] > 0, 'load CUA provenance')
require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'load external ACK time order')
require(observed['status'] == '手動セーブを読み込みました。', 'normal load result')
return ['distinct_pid', 'formal_members', 'load_ack']
def inspect_os_requests(directory, layout, role, pid, root=ROOT):
pair = directory/'os-pair'/layout
def path(name):
result = directory/name
require(result.resolve().is_relative_to(directory.resolve()) and not result.is_symlink(), 'OS artifact escape')
return result
def document(name):
return json.loads(path(name).read_bytes())
listing = json.loads((pair/(role+'-requests.json')).read_bytes())
require(listing['pid'] == pid and listing['requests'] and len(set(listing['requests'])) == len(listing['requests']), 'OS request list')
bindings = json.loads((pair/(role+'-screenshots.json')).read_bytes())
seen, records, pixel_categories = set(), [], set()
stream = pair/(role+'-stream')
captures = {row['label']: row for row in map(json.loads, (stream/'manifest.jsonl').read_text().splitlines()) if 'report' in row}
prior_tick = -1
for sequence, ack_name in enumerate(listing['requests'], 1):
ack_path = path(ack_name)
request_path = ack_path.with_name('request.json')
observed = json.loads(ack_path.with_name('observed.json').read_bytes())
request, ack = json.loads(request_path.read_bytes()), json.loads(ack_path.read_bytes())
cua = document(ack['input_receipt'])
require(request['sequence'] == ack['sequence'] == observed['sequence'] == sequence, 'OS request sequence')
require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == pid, 'OS request PID')
digest = sha(request_path)
require(ack['request_sha256'] == observed['request_sha256'] == digest, 'OS ACK hash')
require(prior_tick < request['requested_ticks_usec'] < observed['ack_observed_ticks_usec'], 'OS ACK order')
prior_tick = observed['ack_observed_ticks_usec']
require(request['requested_unix_ns'] <= cua['timestamp_ns'] <= ack['timestamp_ns'] <= observed['ack_observed_unix_ns'], 'OS external ACK time order')
require(cua['method'] == 'native_os' and cua['tool_name'] and cua['tool_call_id']
and cua['timestamp_ns'] > 0 and cua['collector_pid'] > 0 and cua['collector_pid'] != pid, 'OS CUA provenance')
require('arguments' in cua and isinstance(cua['arguments'], dict), 'full CUA arguments required')
if request['action'] != 'screenshot':
require(cua['events'] and observed['events'], 'OS input events missing')
if request['action'] == 'wheel':
require(request['ctrl'] is False and any(e.get('ctrl') is False and e.get('pressed') for e in observed['events']), 'ordinary wheel evidence')
report = decode(request['report'])
capture = captures[request_path.parent.name]
close(read_object(stream, capture['report']), report, 'request draw report binding')
for number, shot in enumerate(cua['screenshots']):
matches = [b for b in bindings if b['request'] == str(request_path.relative_to(directory)) and b['screenshot_index'] == number]
require(len(matches) == 1, 'screenshot binding coverage')
binding = matches[0]
identity = (binding['request'], number)
require(identity not in seen, 'duplicate screenshot binding')
seen.add(identity)
require(binding['request_sha256'] == digest and binding['target_pid'] == pid and binding['sequence'] == sequence,
'screenshot request attribution')
require(binding['input_receipt'] == ack['input_receipt'] and binding['input_receipt_sha256'] == sha(path(ack['input_receipt'])), 'screenshot CUA receipt hash')
require(binding['raw_path'] == shot['path'] and binding['raw_sha256'] == shot['raw_sha256'] == sha(path(shot['path']))
and binding['tool_call_id'] == shot['tool_call_id'] and shot['tool_call_id'], 'screenshot raw provenance')
conversion = document(binding['conversion']) if binding['conversion'] else None
image = screenshot_pixels(path(shot['path']), conversion, shot)
require(binding['png_sha256'] == sha(path(binding['png_path'])) and (binding['width'], binding['height']) == image[:2], 'decoded screenshot binding')
if conversion:
require(Path(conversion['png_path']).resolve() == path(binding['png_path']).resolve(), 'converted output binding')
for suffix in ('stdout', 'stderr'):
require(sha(path(binding['conversion']+'.'+suffix)) == conversion[suffix+'_sha256'], 'converter log hash')
else:
require(binding['png_path'] == binding['raw_path'], 'PNG must bind original bytes')
# Screenshot actions observe a stable report; input before/after
# screenshots are provenance only and never relabelled as this frame.
if request['action'] == 'screenshot':
checked = compare_rectangles(report, decode(request['rectangles']), request['local_to_viewport'],
request['viewport_to_screen'], request['local_to_screen'], image, root)
pixel_categories.update(c.split('/')[0] for c in checked)
records.append(dict(request=request, ack=ack, observed=observed, cua=cua, request_sha256=digest))
require(len(seen) == len(bindings), 'unattributed screenshot bindings')
return records, pixel_categories
def inspect_native_phases(directory, index, root=ROOT):
runner = module('p23_existing_runner_checks', root/'tools/run_all_tests.py')
expected = ['runs/'+name+'/'+name+'-receipt.json' for name in ('import', 'parse', 'layout-A', 'layout-B')]
require(index['phases'] == expected, 'native phase coverage/order')
for name in expected:
path = directory/name
receipt = json.loads(path.read_bytes())
pins = json.loads(path.with_name('p23-inputs.json').read_bytes())
require(pins['receipt_sha256'] == sha(path), 'native receipt pin')
require(pins['source_map'] == index['source_map'] and {k: v['sha256'] for k, v in pins['source_after'].items()} == index['source_map'], 'native sources changed')
require(pins['inputs'] == pins['inputs_after'] == input_map(root) and pins['spec_sha256'] == SPEC_SHA, 'native input applicability')
require(pins['default_before'] == pins['default_after'] and pins['stage_before'] == pins['stage_after'], 'native profile/stage changed')
require(pins['settings_sha256'] == sha(root/'project.godot'), 'native settings changed')
require(receipt['passed'] is True and receipt['failed'] is False and receipt['engine_ended'] is True
and receipt['exit_code'] == 0 and not receipt.get('validation_error') and not receipt.get('guard_error'), 'native process failed')
capture = receipt['outer_actual_capture']
require(capture['passed'] is True and capture['all_captured_ended'] is True, 'native owned cleanup')
require(receipt['whole_startup_profile_after']['fresh_before_engine'] is True, 'native profile protection')
require(receipt['default_player_entire_profile_unchanged'] is True, 'native default profile changed')
require(isinstance(receipt['controlled_environment'], dict), 'native environment absent')
diagnostics = receipt['finite_original_diagnostic_review']
require(runner.process_gate(receipt['native_process'], diagnostics['allowed_e31_error_lines']), 'native process gate')
if receipt['test']:
require(receipt['source_sha256'] == receipt['source_sha256_end'] == sha(root/receipt['test'])
and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker')
def check(root, directory):
"""Never infer completion of unimplemented gates from an index assertion."""
deficits = []
executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS}
semantic, pixel_categories, anchors = set(), set(), set()
index_path = directory / 'index.json'
if not index_path.is_file():
return ['missing evidence index: ' + str(index_path)]
try:
index = json.loads(index_path.read_bytes())
runner = module('p23_source_applicability', root/'tools/run_all_tests.py')
require(index.get('source_map') == {n: v['sha256'] for n, v in runner.source_refs(root).items()}, 'exact frozen source map')
if index.get('input_map') != input_map(root):
deficits.append('normative input_map differs from current immutable inputs')
if index.get('spec_sha256') != SPEC_SHA:
deficits.append('immutable spec SHA missing or different')
if index.get('marker_contract_sha256') != sha(root / CONTRACT):
deficits.append('marker contract SHA missing or different')
artifacts = index.get('artifacts', {})
if not artifacts:
deficits.append('missing indexed evidence artifacts')
for name, digest in artifacts.items():
path = directory / name
require(path.resolve().is_relative_to(directory.resolve()) and not path.is_symlink(),
'artifact escapes evidence directory')
require(path.is_file() and sha(path) == digest, 'artifact SHA mismatch: ' + name)
inspect_native_phases(directory, index, root)
for layout in ('A', 'B'):
layout_semantic, layout_pixels = set(), set()
stream = directory / ('layout-' + layout)
if (stream / 'manifest.jsonl').is_file():
inspect_stream(stream, root)
else:
deficits.append('missing native layout-' + layout + ' typed diagnostic stream')
for role in ('writer', 'reader'):
stream = directory / 'os-pair' / layout / (role + '-stream')
if (stream / 'manifest.jsonl').is_file():
inspect_stream(stream, root)
# Successful independent recalculation, with nonempty
# observed rows required before a category earns coverage.
for line in (stream/'manifest.jsonl').read_text().splitlines():
capture = json.loads(line)
if 'report' not in capture: continue
report = read_object(stream, capture['report'])
presence = {'room': report['rooms'], 'queue': report['landing_badges'],
'EV': report['cabins'], 'staff': report['staff'],
'goods': report['goods']['requests'], 'business': report['badges']}
semantic.update('semantic_'+c for c, rows in presence.items() if rows)
layout_semantic.update(c for c, rows in presence.items() if rows)
if any(r['people'] for r in report['rooms'].values()): anchors.add('room_users')
if any(r['overflow'] > 0 for r in report['landing_badges']): anchors.add('queue_overflow')
if any(r['passengers'] for r in report['cabins']): anchors.add('elevator_passengers')
if report['staff'] and report['tasks']: anchors.add('staff_jobs')
if any(r['carried_inventory_id'] for r in report['staff']) and report['goods']['requests']: anchors.add('carried_goods')
if role == 'reader': anchors.add('resume_recalculation')
else:
deficits.append('missing OS ' + layout + '-' + role + ' typed diagnostic stream')
ended = directory / 'os-runs' / (layout + '-' + role) / 'ended.json'
if ended.is_file():
receipt = json.loads(ended.read_bytes())
inspect_os_receipt(receipt, index.get('source_map'), input_map(root))
records, categories = inspect_os_requests(directory, layout, role, receipt['actual_pid'], root)
pixel_categories.update('pixels_'+c for c in categories if c in CATEGORIES)
layout_pixels.update(categories)
if role == 'reader':
writer = json.loads((directory/'os-runs'/(layout+'-writer')/'ended.json').read_bytes())
pair = directory/'os-pair'/layout
members = {p.name: sha(p) for p in (pair/'formal').iterdir() if p.is_file()}
inspect_resume(writer, receipt, json.loads((pair/'save.json').read_bytes()), records, members)
require((pair/'writer-formal-model.var').read_bytes() == (pair/'reader-formal-model.var').read_bytes(), 'resumed whole formal model')
anchors.add('distinct_pid_resume')
else:
deficits.append('missing OS ' + layout + '-' + role + ' process receipt')
for category in CATEGORIES:
if category not in layout_semantic or category not in layout_pixels:
deficits.append('missing actual OS ' + layout + ' semantic/pixel coverage: ' + category)
if all((directory / ('layout-' + layout) / 'manifest.jsonl').is_file() for layout in ('A', 'B')):
compare_layouts(comparison_records(directory / 'layout-A'), comparison_records(directory / 'layout-B'))
anchors.add('route_improvement')
if not deficits:
anchors.add('os_guards')
for condition in CONDITIONS:
for category in CATEGORIES:
executed[condition][category].update((semantic | pixel_categories | anchors) & REQUIRED_CHECKS[condition][category])
deficits.extend(condition + ': ' + json.dumps(missing, ensure_ascii=False) for condition, missing in BLOCKERS(executed).items())
except (OSError, ValueError, KeyError, TypeError, AttributeError) as exc:
deficits.append('invalid evidence index: ' + str(exc))
return deficits
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--verify-only', action='store_true')
parser.add_argument('--project-root', type=Path, default=ROOT)
parser.add_argument('--diagnostics', type=Path)
parser.add_argument('--layout-a', type=Path)
parser.add_argument('--layout-b', type=Path)
args = parser.parse_args(argv)
if args.verify_only:
build = os.environ.get('BUILD_OUTPUT_DIR')
if not build:
print('BUILD_OUTPUT_DIR is required', file=sys.stderr)
return 2
deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence')
print(json.dumps(dict(task_id='P23-03', integrity='fail' if deficits else 'pass',
deficits=deficits, acceptance='pending'), ensure_ascii=False))
return 1 if deficits else 0
# Registration hashes the immutable JSON after trimming outer whitespace.
require(hashlib.sha256((ROOT / SPEC).read_text().strip().encode()).hexdigest() == SPEC_SHA,
'immutable spec mismatch')
+343 -14
View File
@@ -1,25 +1,354 @@
#!/usr/bin/env python3
"""Read-only P23 readiness report. Collection is blocked in this candidate.
No runner, engine, profile, subprocess, browser or OS input is started. Root must
not mistake this preparation interface for a submitted native/OS collector.
"""
"""Root-only native collection. Does not run regression or claim acceptance."""
import argparse
import hashlib
import importlib.util
import json
import os
from pathlib import Path
import shutil
import subprocess
import sys
import time
import uuid
import re
sys.dont_write_bytecode = True
from check_p23_life_observation_evidence import condition_matrix
HERE = Path(__file__).resolve().parent
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
CONDITIONS = ('room_occupancy_and_operation', 'queues_and_overflow',
'elevator_state_and_passengers', 'staff_state_and_jobs',
'supplies_and_carried_goods', 'same_demand_route_improvement',
'normal_os_and_distinct_pid_resume')
OS_CHILD_TIMEOUT_SECONDS = 900
RECORDER = 'tests/fixtures/p23-03/passive_os_recorder.gd'
def collect_screenshots(out, pair, role):
"""Bind every external CUA screenshot to its request and decoded PNG.
def main():
print(json.dumps({'task_id': 'P23-03', 'status': 'blocked', 'engine_runs': 0,
'condition_matrix': condition_matrix(),
'required_infrastructure': ['run_all_tests.py', 'strict_test_markers.py',
'regression/owned_outer.py',
'regression/ordinary_proof_gate.py'],
'root_evidence': 'missing'}, ensure_ascii=False))
return 2
The external receipt supplies screenshots[{path,raw_sha256,tool_call_id}].
Paths are relative to the evidence root; raw receipt bytes remain unchanged.
"""
checker = module('p23_screenshot_binding', HERE/'check_p23_life_observation_evidence.py')
bindings = []
requests = json.loads((pair/(role+'-requests.json')).read_text())['requests']
for ack_name in requests:
ack_path = out/ack_name
checker.require(ack_path.resolve().is_relative_to(out.resolve()), 'ACK path escape')
ack = json.loads(ack_path.read_text())
request_path = ack_path.with_name('request.json')
request = json.loads(request_path.read_text())
receipt_path = out/ack['input_receipt']
checker.require(receipt_path.resolve().is_relative_to(out.resolve()), 'CUA receipt path escape')
cua = json.loads(receipt_path.read_text())
checker.require(ack['request_sha256'] == checker.sha(request_path), 'CUA request hash')
checker.require(cua['screenshots'], 'CUA screenshots missing')
for number, shot in enumerate(cua['screenshots']):
raw = out/shot['path']
checker.require(raw.resolve().is_relative_to(out.resolve()) and not raw.is_symlink(), 'screenshot path escape')
checker.require(checker.sha(raw) == shot['raw_sha256'] and shot['tool_call_id'], 'raw screenshot CUA binding')
kind, _, _ = checker.image_kind(raw.read_bytes())
conversion = None
if kind == 'jpeg':
png = ack_path.with_name('decoded-%02d.png' % number)
receipt = ack_path.with_name('conversion-%02d.json' % number)
convert_screenshot(raw.resolve(), png.resolve(), shot, receipt.resolve())
conversion = str(receipt.relative_to(out))
else:
png = raw
width, height, _ = checker.png_pixels(png.read_bytes())
bindings.append(dict(request=str(request_path.relative_to(out)), request_sha256=checker.sha(request_path),
sequence=request['sequence'], target_pid=request['target_pid'],
input_receipt=ack['input_receipt'], input_receipt_sha256=checker.sha(receipt_path),
screenshot_index=number, tool_call_id=shot['tool_call_id'],
raw_path=shot['path'], raw_sha256=shot['raw_sha256'], conversion=conversion,
png_path=str(png.relative_to(out)), png_sha256=checker.sha(png), width=width, height=height))
checker.require(bindings, 'empty screenshot collection')
destination = pair/(role+'-screenshots.json')
with destination.open('x') as output:
json.dump(bindings, output, indent=2)
output.write('\n')
return str(destination.relative_to(out))
def convert_screenshot(raw_path, png_path, cua_receipt, receipt_path):
"""Root-only conversion; raw CUA bytes are never renamed or rewritten."""
checker = module('p23_conversion_checker', HERE/'check_p23_life_observation_evidence.py')
raw, png, receipt = map(Path, (raw_path, png_path, receipt_path))
checker.require(raw.is_absolute() and png.is_absolute() and receipt.is_absolute(),
'absolute conversion paths required')
checker.require(raw.is_file() and not raw.is_symlink() and not png.exists()
and not receipt.exists() and raw.resolve() != png.resolve(), 'fresh separate conversion output')
raw_hash = checker.sha(raw)
checker.require(raw_hash == cua_receipt['raw_sha256'] and cua_receipt['tool_call_id'],
'CUA conversion input hash/call mismatch')
kind, width, height = checker.image_kind(raw.read_bytes())
checker.require(kind == 'jpeg', 'conversion requires JPEG input')
converter = '/usr/bin/sips'
converter_hash = checker.sha(converter)
argv = [converter, '-s', 'format', 'png', str(raw), '--out', str(png)]
process = subprocess.run(argv, capture_output=True, timeout=900, check=False)
rec = dict(raw_path=str(raw), raw_sha256=raw_hash, raw_kind=kind,
tool_call_id=cua_receipt['tool_call_id'], converter=converter,
converter_sha256=converter_hash, argv=argv, exit_code=process.returncode,
stdout_sha256=hashlib.sha256(process.stdout).hexdigest(),
stderr_sha256=hashlib.sha256(process.stderr).hexdigest(),
png_path=str(png), png_sha256=checker.sha(png) if png.is_file() else None,
width=width, height=height)
# Preserve failure receipts too. These logs allow independent hash checking.
with receipt.open('x') as output:
json.dump(rec, output, indent=2)
output.write('\n')
with Path(str(receipt) + '.stdout').open('xb') as output:
output.write(process.stdout)
with Path(str(receipt) + '.stderr').open('xb') as output:
output.write(process.stderr)
checker.require(checker.sha(raw) == raw_hash, 'raw CUA bytes changed during conversion')
checker.require(checker.sha(converter) == converter_hash, 'converter changed during conversion')
checker.inspect_conversion(rec, cua_receipt, converter_hash)
return rec
def collect_os(r, root, out, stage, engine, commit, refs, protected, outer):
"""Adapted P08-03 collect_os protocol, retaining owned-process guards.
Root supplies CUA ack/receipt files in the requested directories. Neither
controller nor passive endpoints generate input or change permissions.
"""
r.require(not outer.native.helper['live_engines'](), 'external engine present')
checker = module('p23_os_pins', HERE/'check_p23_life_observation_evidence.py')
before = r.inventory(stage, stage=True)
public = (stage/'project.godot').read_bytes()
(out/'os-pair').mkdir()
phases = []
for label in ('A-writer', 'A-reader', 'B-writer', 'B-reader'):
layout, role = label.split('-')
pair = out/'os-pair'/layout
pair.mkdir(exist_ok=role == 'reader')
r.require(not outer.native.helper['live_engines'](), 'external engine present')
r.require(r.source_refs(root) == refs and r.default_player(root) == protected, 'OS before guards')
r.require(r.inventory(stage, stage=True) == before, 'OS stage before guard')
r.require(shutil.disk_usage(out).free >= 500*1024*1024, 'OS disk admission')
folder = out/'os-runs'/label
folder.mkdir(parents=True)
name = 'TheTree-PCK-tests-'+uuid.uuid4().hex+'-p23-observation-'+label
profile = r.profile_path(name)
r.require(not profile.exists(), 'fresh OS private profile')
formal_source = pair/'formal' if role == 'reader' else None
formal_before = r.inventory(formal_source) if formal_source else {}
if role == 'reader':
r.require(set(formal_before) == {'world.bin','manifest.json','thumbnail.png','journal.log'}, 'all formal inputs')
formal = profile/'saves/manual_ui'
formal.mkdir(parents=True)
for member, pin in formal_before.items():
r.copy_source(formal_source/member, formal/member)
r.require(r.sha(formal/member) == pin['sha256'], 'formal fixture copy SHA')
recorder = folder/'passive_os.gd'
r.copy_source(root/RECORDER, recorder)
proof = folder/'teardown'
(proof/'owned-audio').mkdir(parents=True)
env = r.fresh_environment(dict(os.environ))
env.update(P23_OBSERVATION_ROOT=str(pair), P23_OBSERVATION_ROLE=role, P23_LAYOUT=layout,
THE_TREE_AUDIO_TEARDOWN_PROOF_DIR=str(proof/'owned-audio'),
THE_TREE_AUDIO_FIXTURE_CONTROL_PROOF=str(proof/'control.json'))
command = [engine, '--path', str(stage), '--script', str(recorder)]
base = {'protocol':'p23-observation-native-os-v1', 'role':role, 'layout':layout, 'commit':commit,
'source_map':{n:v['sha256'] for n,v in refs.items()}, 'stage':str(stage),
'stage_before':before, 'profile':str(profile), 'profile_name':name,
'profile_before':{'existed':profile.exists(), 'files':r.inventory(profile) if profile.exists() else {}}, 'formal_input':formal_before,
'formal_source':str(formal_source) if formal_source else None, 'owner_pid':os.getpid(),
'default_before':protected, 'command':command, 'engine_sha256':r.sha(engine),
'controller_sha256':r.sha(root/'tools/run_p23_life_observation.py'),
'recorder_sha256':r.sha(recorder), 'formal_acceptance':False,
'timeout_seconds':OS_CHILD_TIMEOUT_SECONDS,
'inputs':checker.input_map(root), 'spec_sha256':checker.SPEC_SHA,
'settings_sha256':hashlib.sha256(public).hexdigest(),
'environment':{k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
'environment_sha256':hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest()}
r.write(folder/'started.json', base)
error = None; children = []; guards = []; exit_code = None; owner_proof = None
try:
(stage/'project.godot').write_bytes(r.private_project(public, name))
with (folder/'engine-native.log').open('x') as log:
process = subprocess.Popen(command, cwd=stage, env=env, stdout=log, stderr=subprocess.STDOUT)
children.append(process)
r.write(folder/'process-started.json', dict(base, actual_pid=process.pid,
monotonic_ns=time.monotonic_ns(), state='waiting_for_root_cua_requests'))
print(json.dumps({'actual_pid':process.pid, 'request_directory':str(pair),
'state':'waiting_for_root_cua', 'role':role}), flush=True)
exit_code = process.wait(timeout=base['timeout_seconds'])
r.require(exit_code == 0, 'ordinary native OS exit0')
raw = (folder/'engine-native.log').read_text()
r.require('ERROR:' not in raw and 'SCRIPT ERROR:' not in raw, 'OS native diagnostics')
matches = re.findall(r'(?m)^P23 passive OS observation: ([0-9]+) checks, 0 failures$', raw)
r.require(len(matches) == 1 and int(matches[0]) > 0, 'passive observer completion')
observed = r.read(pair/(role+'-profile.json'))
r.require(observed['pid'] == process.pid and Path(observed['user_data_dir']).resolve() == profile.resolve()
and observed['display'] != 'headless', 'actual OS private profile/display')
collect_screenshots(out, pair, role)
owners = r.module('p23_os_owner_gate', r.SUPPORT/'ordinary_proof_gate.py')
owner_proof = owners.ordinary_gate(proof, process.pid, folder, ['single'])
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
try:
cleanup, cleanup_errors = outer.finish_captured(children)
finally:
(stage/'project.godot').write_bytes(public)
try:
r.require(r.source_refs(root) == refs, 'OS source unchanged')
r.require(r.inventory(stage, stage=True) == before, 'OS stage unchanged')
r.require(r.default_player(root) == protected, 'OS default player unchanged')
if formal_source:
r.require(r.inventory(formal_source) == formal_before, 'formal input unchanged')
r.require(r.sha(recorder) == base['recorder_sha256'], 'recorder unchanged')
except BaseException as caught:
guards.append(type(caught).__name__+': '+str(caught))
passed = not (error or guards or cleanup_errors) and len(children) == 1 and exit_code == 0
r.write(folder/'ended.json', dict(base, actual_pid=children[0].pid if children else None,
exit_code=exit_code, engine_ended=bool(cleanup) and all(v['engine_ended'] for v in cleanup),
error=error, guard_errors=guards, cleanup_errors=cleanup_errors, owned_cleanup=cleanup,
ordinary_owner_proof=owner_proof, stage_after=r.inventory(stage,stage=True),
engine_sha256_after=r.sha(engine), inputs_after=checker.input_map(root),
settings_sha256_after=hashlib.sha256((stage/'project.godot').read_bytes()).hexdigest(),
environment_after={k:v for k,v in env.items() if k.startswith(('P23_', 'THE_TREE_'))},
environment_sha256_after=hashlib.sha256(json.dumps(env,sort_keys=True).encode()).hexdigest(),
default_after=r.default_player(root), source_after=r.source_refs(root),
profile_after=r.inventory(profile), passed=passed))
phases.append('os-runs/'+label+'/ended.json')
r.require(passed, 'native OS collection failed: '+str(error or guards or cleanup_errors))
return phases
def module(name, path):
spec = importlib.util.spec_from_file_location(name, path)
obj = importlib.util.module_from_spec(spec)
spec.loader.exec_module(obj)
return obj
def absolute(value):
path = Path(value)
if not path.is_absolute():
raise argparse.ArgumentTypeError('absolute path required')
return path.resolve()
def collect(args):
root, out = args.project_root, args.output
if out.is_relative_to(root):
raise ValueError('evidence output must be outside the frozen source checkout')
r = module('p23_standard_runner', root/'tools/run_all_tests.py')
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
r.require(commit == args.commit, 'actual final submitted commit required')
r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required')
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
refs = r.source_refs(root)
checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py')
inputs_before = checker.input_map(root)
outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py')
r.require(not outer.native.helper['live_engines'](), 'one native engine')
engine = shutil.which(args.godot)
r.require(engine is not None, 'native engine exists')
engine = str(Path(engine).resolve())
out.mkdir(parents=True)
stage = out/'stage'; stage.mkdir()
(out/'starters').mkdir(); (out/'bootstrap').mkdir()
for name, pin in r.read(r.SUPPORT/'bootstrap-scripts.json').items():
data = pin['text'].encode()
r.require(hashlib.sha256(data).hexdigest() == pin['sha256'], 'bootstrap pin')
(out/'bootstrap'/name).write_bytes(data)
for name, pin in refs.items():
target = stage/name; target.parent.mkdir(parents=True, exist_ok=True)
r.copy_source(Path(pin['path']), target)
r.require(r.sha(target) == pin['sha256'], 'whole copied source '+name)
expected = {n: {'bytes':v['bytes'], 'sha256':v['sha256']} for n,v in refs.items()}
selected = {'root_before':refs, 'map':{n:v['sha256'] for n,v in refs.items()}, 'timeout':180}
r.require(r.inventory(stage, stage=True) == expected, 'complete private source')
protected = r.default_player(root); public = (stage/'project.godot').read_bytes()
paths = ['res://'+n for n in sorted(refs) if n.endswith('.gd')]
r.write(out/'parse-paths.json', paths)
markers = r.module('p23_strict_markers', root/'tools/strict_test_markers.py')
contracts = markers.load_contracts(root/CONTRACT)
policy = r.read(r.SUPPORT/'suite-policy.json')
diagnostics = r.module('p23_diagnostics', r.SUPPORT/'diagnostic_gate.py')
owners = r.module('p23_owners', r.SUPPORT/'ordinary_proof_gate.py')
audio = r.module('p23_audio', r.SUPPORT/'audio_evidence_gate.py')
r.write(out/'runner-input.json', {'protocol':'p23-normal-observation-v1', 'commit':commit,
'selected_source_map':selected['map'], 'engine':engine, 'engine_sha256':r.sha(engine),
'owner_pid':os.getpid(), 'default_player':protected, 'diagnostic_seed':806042,
'inputs':inputs_before, 'provenance':checker.input_provenance(root),
'pixel_decoder':{'interpreter':sys.executable,
'PIL_available':importlib.util.find_spec('PIL') is not None,
'raw_screenshots':'preserve bytes; Root lossless conversion required for JPEG without PIL'}})
phases = []; previous = {}; cases = []; error = None; os_proof = None
deadline = time.monotonic()+900
saved_env = {k:os.environ.get(k) for k in ('P23_LAYOUT','P23_OUTPUT')}
def phase(label, test):
remaining = int(deadline-time.monotonic())
r.require(remaining >= (180 if label == 'import' else 1), '900s collection deadline')
selected['timeout'] = min(180, remaining)
phase_out = out/'runs'/label
phase_out.mkdir(parents=True)
(phase_out/'starters').mkdir(); (phase_out/'bootstrap').mkdir()
for original in (out/'bootstrap').iterdir():
r.copy_source(original,phase_out/'bootstrap'/original.name)
r.require(r.sha(original)==r.sha(phase_out/'bootstrap'/original.name),'bootstrap full copy')
r.write(phase_out/'parse-paths.json',paths)
row = r.execute_phase(engine, stage, phase_out, label, test, selected, expected, previous,
policy, contracts, paths, public, protected, root, outer, markers, diagnostics, owners, audio)
# Supplement the existing runner receipt; preserve it verbatim.
r.write(phase_out/'p23-inputs.json', dict(source_map=selected['map'], source_after=r.source_refs(root),
engine_sha256=r.sha(engine), inputs=inputs_before, inputs_after=checker.input_map(root),
spec_sha256=checker.SPEC_SHA, settings_sha256=hashlib.sha256(public).hexdigest(),
stage_before=expected, stage_after=r.inventory(stage,stage=True),
default_before=protected, default_after=r.default_player(root),
receipt_sha256=r.sha(phase_out/(label+'-receipt.json'))))
phases.append('runs/'+label+'/'+label+'-receipt.json')
r.require(not row['failed'], 'strict phase failed: '+label)
try:
phase('import', None); phase('parse', None)
for layout in ('A', 'B'):
directory = out/('layout-'+layout)
os.environ.update(P23_LAYOUT=layout, P23_OUTPUT=str(directory))
phase('layout-'+layout, 'tests/ui/test_p23_life_observation.gd')
cases.append({'directory':directory.name, 'phase':phases[-1], 'files':r.inventory(directory)})
if args.input_mode == 'native_os':
os_phases = collect_os(r, root, out, stage, engine, commit, refs, protected, outer)
checker = module('p23_os_input_map', root/'tools/check_p23_life_observation_evidence.py')
os_proof = 'os-proof.json'
r.write(out/os_proof, {'source_map':selected['map'], 'inputs':checker.input_map(root),
'input_method':'native_os', 'observer_mutations':[], 'phases':os_phases, 'directory':'os-pair'})
except BaseException as caught:
error = type(caught).__name__+': '+str(caught)
finally:
cleanup, cleanup_errors = outer.finish_captured(outer.OWNED)
for key,value in saved_env.items():
if value is None: os.environ.pop(key, None)
else: os.environ[key] = value
if r.source_refs(root) != refs or r.default_player(root) != protected:
error = (error+'; ' if error else '')+'protected root/profile changed'
if checker.input_map(root) != inputs_before:
error = (error+'; ' if error else '')+'spec/fixture/controller inputs changed'
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py')
r.write(out/'index.json', {'schema':'p23-life-observation-v1', 'commit':commit,
'source_map':selected['map'], 'input_map':inputs_before,
'provenance':checker.input_provenance(root),
'marker_contract_sha256':r.sha(root/CONTRACT),
'spec_sha256':hashlib.sha256((root/'docs/spec/acceptance/P23-03.json').read_text().strip().encode()).hexdigest(),
'cases':cases, 'phases':phases, 'conditions':list(CONDITIONS),
'artifacts':{str(p.relative_to(out)):r.sha(p)
for p in out.rglob('*') if p.is_file() and 'stage' not in p.relative_to(out).parts},
'os_proof':os_proof, 'regression':None,
'acceptance':'pending', 'collection_error':error})
return 1 if error or cleanup_errors else 0
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--project-root', type=absolute, required=True)
parser.add_argument('--output', type=absolute, required=True)
parser.add_argument('--commit', required=True)
parser.add_argument('--godot', default='godot')
parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler')
parser.add_argument('--collect', action='store_true', required=True)
return collect(parser.parse_args(argv))
if __name__ == '__main__':
raise SystemExit(main())
+307
View File
@@ -3,11 +3,318 @@
import copy
import unittest
import sys
import contextlib
import io
import json
import hashlib
import struct
import zlib
import tempfile
from pathlib import Path
from unittest import mock
sys.dont_write_bytecode = True
import check_p23_life_observation_evidence as check
class ImageTests(unittest.TestCase):
def test_collector_conversion_is_bound_to_each_cua_screenshot(self):
runner = check.module('p23_unit_collector', check.ROOT/'tools/run_p23_life_observation.py')
with tempfile.TemporaryDirectory(dir=check.ROOT/'tests/fixtures/p23-03') as temporary:
root = Path(temporary)
pair = root/'os-pair/A'
request_dir = pair/'writer-request-0001'
request_dir.mkdir(parents=True)
request = request_dir/'request.json'
request.write_text(json.dumps(dict(sequence=1, target_pid=123)))
raw = request_dir/'raw.jpg'
raw.write_bytes(b'\xff\xd8unit-only\xff\xd9')
shot = dict(path=str(raw.relative_to(root)), raw_sha256=check.sha(raw), tool_call_id='unit-shot-call')
receipt = request_dir/'cua.json'
receipt.write_text(json.dumps(dict(screenshots=[shot])))
ack = request_dir/'ack.json'
ack.write_text(json.dumps(dict(request_sha256=check.sha(request), input_receipt=str(receipt.relative_to(root)))))
(pair/'writer-requests.json').write_text(json.dumps(dict(requests=[str(ack.relative_to(root))])))
def convert(raw_path, png_path, cua, receipt_path):
self.assertEqual(cua, shot)
self.assertEqual(Path(raw_path).read_bytes(), b'\xff\xd8unit-only\xff\xd9')
Path(png_path).write_bytes(self.png())
Path(receipt_path).write_text('{}')
with mock.patch.object(runner, 'module', return_value=check), \
mock.patch.object(check, 'image_kind', return_value=('jpeg', 2, 2)), \
mock.patch.object(runner, 'convert_screenshot', side_effect=convert) as converter:
output = runner.collect_screenshots(root, pair, 'writer')
converter.assert_called_once()
rows = json.loads((root/output).read_text())
self.assertEqual(rows[0]['tool_call_id'], shot['tool_call_id'])
self.assertEqual(rows[0]['raw_sha256'], check.sha(raw))
self.assertEqual(rows[0]['png_sha256'], check.sha(root/rows[0]['png_path']))
self.assertTrue(rows[0]['conversion'])
@staticmethod
def png(mode=0, channels=3):
def chunk(kind, body):
return struct.pack('>I', len(body)) + kind + body + struct.pack('>I', zlib.crc32(kind + body))
rows = [bytes([10, 20, 30, 40][:channels] * 2),
bytes([50, 60, 70, 80][:channels] * 2)]
filtered = bytearray()
prior = bytes(2 * channels)
for row in rows:
filtered.append(mode)
for x, value in enumerate(row):
left, up, corner = (row[x-channels] if x >= channels else 0,
prior[x], prior[x-channels] if x >= channels else 0)
p = left + up - corner
distances = [abs(p-left), abs(p-up), abs(p-corner)]
predictors = [0, left, up, (left+up)//2,
[left, up, corner][distances.index(min(distances))]]
filtered.append((value-predictors[mode]) % 256)
prior = row
header = struct.pack('>IIBBBBB', 2, 2, 8, 2 if channels == 3 else 6, 0, 0, 0)
return (b'\x89PNG\r\n\x1a\n' + chunk(b'IHDR', header)
+ chunk(b'IDAT', zlib.compress(filtered)) + chunk(b'IEND', b''))
def test_all_png_filters_rgb_and_rgba(self):
for mode in range(5):
for channels in (3, 4):
with self.subTest(mode=mode, channels=channels):
w, h, rows = check.png_pixels(self.png(mode, channels))
self.assertEqual((w, h), (2, 2))
self.assertEqual(rows[0][0], tuple([10, 20, 30, 40][:channels]))
self.assertEqual(rows[1][1], tuple([50, 60, 70, 80][:channels]))
def test_crc_and_trailing_bytes_rejected(self):
data = bytearray(self.png())
data[29] ^= 1
with self.assertRaisesRegex(ValueError, 'CRC mismatch'):
check.png_pixels(bytes(data))
with self.assertRaisesRegex(ValueError, 'invalid IEND'):
check.png_pixels(self.png() + b'extra')
def conversion(self):
raw, png = b'\xff\xd8synthetic-unit-test\xff\xd9', self.png()
digest = lambda b: hashlib.sha256(b).hexdigest()
rec = dict(raw_path='/raw.png', raw_sha256=digest(raw), raw_kind='jpeg',
tool_call_id='unit-test-call', converter='/usr/bin/sips', converter_sha256='a'*64,
argv=['/usr/bin/sips', '-s', 'format', 'png', '/raw.png', '--out', '/decoded.png'],
exit_code=0, stdout_sha256=digest(b''), stderr_sha256=digest(b''),
png_path='/decoded.png', png_sha256=digest(png), width=2, height=2)
cua = dict(raw_sha256=digest(raw), tool_call_id='unit-test-call')
return rec, cua, {'/raw.png': raw, '/decoded.png': png}
def test_conversion_chain_and_specific_forgery_deficits(self):
rec, cua, files = self.conversion()
helper = mock.Mock()
helper.jpeg_dimensions.return_value = (2, 2)
with mock.patch.object(Path, 'read_bytes', lambda p: files[str(p)]), \
mock.patch.object(check, 'module', return_value=helper):
self.assertEqual(check.inspect_conversion(rec, cua, 'a'*64)[:2], (2, 2))
for key, value, deficit in [('converter_sha256', 'b'*64, 'converter SHA mismatch'),
('argv', [], 'converter argv mismatch'),
('raw_sha256', 'b'*64, 'CUA raw hash/call mismatch'),
('width', 3, 'conversion dimensions mismatch')]:
with self.subTest(key=key):
bad = dict(rec, **{key: value})
with self.assertRaisesRegex(ValueError, deficit):
check.inspect_conversion(bad, cua, 'a'*64)
with self.assertRaisesRegex(ValueError, 'JPEG screenshot requires conversion record'):
check.screenshot_pixels('/raw.png')
class RestAuditTests(unittest.TestCase):
def fixture(self):
member = dict(staff_id='s', state='OFF_DUTY', health=100, current_task_id='', current_cell=[0, 0, 0])
facilities = [dict(instance_id=fid, definition_id='staff_waiting_room', origin=[0, 0, 0],
occupied_cells=[], state='OPEN', suspension_reason='') for fid in ('a', 'b')]
runtime = dict(staff=dict(staff=[member]), facilities=dict(instances=facilities),
defense=dict(raids=[]), events=dict(actor_locks={}), elevators=dict(trips=[]),
path=dict(cell_traffic=[dict(cell=[x, 0, 0]) for x in (-1, 0, 1)]),
world=dict(bounds=dict(min_x=-1, max_x=1, min_y=0, max_y=0), occupancies=[]))
routes = [dict(cell=[x, 0, 0], path=[[0, 0, 0], [x, 0, 0]] if x else [[0, 0, 0]],
edges=[dict(**{'from': [0, 0, 0], 'to': [x, 0, 0]}, total_cost=0)] if x else [],
calculate_path_cost=0) for x in (-1, 0, 1)]
observed = []
for f in facilities:
observed.append({k: f[k] for k in ('instance_id', 'definition_id', 'origin', 'occupied_cells')})
observed[-1].update(operation_state='OPERATING', probes=[dict(cell=[x, 0, 0], has_cell=True) for x in (0, -1, 1)],
ordered=[[x, 0, 0] for x in (-1, 0, 1)], routes=copy.deepcopy(routes))
selected = dict(reachable=True, resting=True, cell=[-1, 0, 0], facility_id='a', label='待機室で休憩中')
audit = dict(path_service_sha256=check.sha(check.ROOT/'scripts/world/path_service.gd'),
staff=[dict(member, defense_memberships=[], actor_lock=False, in_transit=False,
facilities=observed, comparison_only=selected)])
report = dict(resting_staff={'s': 'a'}, actor_drawings=[], layer=0, carried_patients={}, rooms={},
viewport=[1440, 810], zoom=1, pan=[0, 0])
return dict(systems=dict(runtime=runtime)), report, audit
def test_selection_ties_and_nonmutation(self):
state, report, audit = self.fixture()
before = copy.deepcopy((state, report, audit))
self.assertEqual(check.compare_rest(state, report, audit)['s']['cell'], [-1, 0, 0])
self.assertEqual((state, report, audit), before)
def test_rest_forgeries(self):
mutations = {
'selection': lambda a: a['staff'][0]['comparison_only'].update(facility_id='b'),
'tie': lambda a: a['staff'][0]['comparison_only'].update(cell=[0, 0, 0]),
'lock': lambda a: a['staff'][0].update(actor_lock=True),
'transit': lambda a: a['staff'][0].update(in_transit=True),
'label': lambda a: a['staff'][0]['comparison_only'].update(label='宿舎で休憩中'),
'cost': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].update(total_cost=9),
'facility_order': lambda a: a['staff'][0]['facilities'].reverse(),
'candidate_order': lambda a: a['staff'][0]['facilities'][0]['ordered'].reverse(),
'empty_edge': lambda a: a['staff'][0]['facilities'][0]['routes'][0]['edges'][0].clear(),
}
for name, mutate in mutations.items():
with self.subTest(name=name):
state, report, audit = self.fixture()
mutate(audit)
with self.assertRaises(ValueError):
check.compare_rest(state, report, audit)
def test_ineligible_and_transit_recomputed(self):
state, report, audit = self.fixture()
state['systems']['runtime']['events']['actor_locks']['s'] = 'event'
audit['staff'][0].update(actor_lock=True, comparison_only={})
report['resting_staff'] = {}
self.assertEqual(check.compare_rest(state, report, audit), {'s': {}})
state, report, audit = self.fixture()
state['systems']['runtime']['elevators']['trips'] = [dict(actor_id='staff:s')]
audit['staff'][0]['in_transit'] = True
audit['staff'][0]['comparison_only'].update(resting=False, label='休憩先へ移動中')
report['resting_staff'] = {}
self.assertFalse(check.compare_rest(state, report, audit)['s']['resting'])
class InfrastructureTests(unittest.TestCase):
def test_matrix_requires_each_condition_and_each_category(self):
executed = {c: {k: set(v) for k, v in categories.items()} for c, categories in check.REQUIRED_CHECKS.items()}
self.assertTrue(all(r['status'] == 'passed' for r in check.condition_matrix(executed)))
for condition in check.CONDITIONS:
for category in check.CATEGORIES:
with self.subTest(condition=condition, category=category):
broken = copy.deepcopy(executed)
broken[condition][category].clear()
matrix = check.condition_matrix(broken)
self.assertEqual(next(r for r in matrix if r['condition'] == condition)['status'], 'pending')
self.assertIn(category, check.BLOCKERS(broken)[condition])
def test_distinct_pid_ack_and_formal_hash_negatives(self):
members = {name: 'a'*64 for name in ('world.bin', 'manifest.json', 'thumbnail.png', 'journal.log')}
writer = dict(actual_pid=1)
reader = dict(actual_pid=2, formal_input={k: dict(sha256=v) for k, v in members.items()})
saved = dict(pid=1, members=members)
records = [dict(request=dict(action='load_menu', target_pid=2, sequence=1, requested_ticks_usec=10, requested_unix_ns=90),
ack=dict(target_pid=2, sequence=1, request_sha256='b'*64, timestamp_ns=110),
observed=dict(pid=2, sequence=1, request_sha256='b'*64, ack_observed_ticks_usec=20, ack_observed_unix_ns=120, status='手動セーブを読み込みました。'),
cua=dict(target_pid=2, method='native_os', tool_call_id='actual-call', timestamp_ns=100),
request_sha256='b'*64)]
check.inspect_resume(writer, reader, saved, records, members)
for change in ('pid', 'ack', 'time', 'hash', 'id'):
with self.subTest(change=change):
r, q = copy.deepcopy(reader), copy.deepcopy(records)
if change == 'pid': r['actual_pid'] = 1
if change == 'ack': q.clear()
if change == 'time': q[0]['observed']['ack_observed_ticks_usec'] = 9
if change == 'hash': r['formal_input']['world.bin']['sha256'] = 'c'*64
if change == 'id': q[0]['ack']['sequence'] = 2
with self.assertRaises(ValueError): check.inspect_resume(writer, r, saved, q, members)
def test_rectangle_pixel_and_transform_negatives(self):
source = 'scripts/ui/pixel_people.gd'
colors = list(dict.fromkeys(check.re.findall(r'Color\("([0-9a-fA-F]{6})"', (check.ROOT/source).read_text())))
report = dict(rooms={}, landing_badges=[], cabins=[], resting_staff={}, badges=[],
actor_drawings=[dict(key='staff/s', staff=True, at=[1, 1], scale=1)])
row = dict(category='staff', id='staff/s', rect=[1, 1, 16, 18], viewport_rect=[1, 1, 16, 18],
screen_rect=[1, 1, 16, 18], style_source=source, style_colors=colors)
audit = dict(rows=[row])
matrix = [1, 0, 0, 1, 0, 0]
image = (24, 24, [[(113, 157, 148)]*24 for _ in range(24)])
self.assertEqual(check.compare_rectangles(report, audit, matrix, matrix, matrix, image), ['staff/staff/s'])
for change in ('rectangle', 'pixels', 'transform', 'coverage', 'palette'):
with self.subTest(change=change):
a, m, img = copy.deepcopy(audit), matrix[:], image
if change == 'rectangle': a['rows'][0]['rect'][0] += 1
if change == 'pixels': img = (24, 24, [[(0, 0, 0)]*24 for _ in range(24)])
if change == 'transform': m[4] = 10
if change == 'coverage': a['rows'].clear()
if change == 'palette': a['rows'][0]['style_colors'] = ['000000']
with self.assertRaises(ValueError): check.compare_rectangles(report, a, matrix, m, matrix, img)
def test_interpolation_history_tampering(self):
state = dict(systems=dict(runtime=dict(guests=dict(guests=[]), staff=dict(staff=[]),
elevators=dict(shafts=[], trips=[], current_minute=0))))
report = dict(animation_time=0.0, paused=True, layer=0, zoom=1, pan=[0,0], viewport=[100,100],
actor_tracks={}, elevator_tracks={}, actor_drawings=[], cabins=[])
audit = dict(frames=[dict(frame=1, delta=.01, scaled_delta=0, display=copy.deepcopy(report)),
dict(frame=2, delta=.01, scaled_delta=0, display=copy.deepcopy(report))])
check.compare_motion(state, report, audit)
for change in ('delta', 'frame', 'display'):
with self.subTest(change=change):
bad = copy.deepcopy(audit)
if change == 'delta': bad['frames'][1]['scaled_delta'] = 1
if change == 'frame': bad['frames'][1]['frame'] = 1
if change == 'display': bad['frames'][1]['display']['animation_time'] = 1
with self.assertRaises(ValueError): check.compare_motion(state, report, bad)
def test_verify_only_missing_index_is_a_concrete_deficit(self):
with mock.patch.dict(check.os.environ, {'BUILD_OUTPUT_DIR': '/missing-p23-proof'}, clear=True):
with contextlib.redirect_stdout(io.StringIO()) as output:
self.assertEqual(check.main(['--verify-only']), 1)
result = json.loads(output.getvalue())
self.assertEqual(result['integrity'], 'fail')
self.assertEqual(result['acceptance'], 'pending')
self.assertIn('index.json', result['deficits'][0])
self.assertNotIn('blocked', result['deficits'][0])
def test_verify_only_requires_build_output(self):
with mock.patch.dict(check.os.environ, {}, clear=True):
with contextlib.redirect_stderr(io.StringIO()) as output:
self.assertEqual(check.main(['--verify-only']), 2)
self.assertIn('BUILD_OUTPUT_DIR is required', output.getvalue())
def test_normative_plan_change_invalidates_but_progress_does_not(self):
root = check.ROOT
text = (root / check.PLAN).read_text()
original = check.input_map(root)
progress = text.splitlines(keepends=True)
progress[2] = 'Progress-only test mutation\n'
with mock.patch.object(Path, 'read_text', return_value=''.join(progress)):
self.assertEqual(check.input_map(root), original)
changed = text.replace('同需要の動線改善を観察できる。', 'altered normative acceptance')
self.assertNotEqual(changed, text)
with mock.patch.object(Path, 'read_text', return_value=changed):
self.assertNotEqual(check.input_map(root), original)
self.assertNotIn(check.PLAN, original)
self.assertIn('full_plan_sha256', check.input_provenance(root))
for path in (root / 'tests/fixtures/p23-03').rglob('*'):
if path.is_file():
self.assertIn(path.relative_to(root).as_posix(), original)
def test_os_process_guards_reject_engine_profile_and_source_drift(self):
source = {'source.gd': 'a' * 64}
receipt = dict(engine_ended=True, passed=True, exit_code=0, error=None,
guard_errors=[], cleanup_errors=[], default_before={}, default_after={},
stage_before={}, stage_after={}, source_map=source,
source_after={'source.gd': {'sha256': 'a' * 64}},
owned_cleanup=[{'engine_ended': True}], ordinary_owner_proof={'present': True})
receipt.update(spec_sha256=check.SPEC_SHA, inputs={'spec': 'a'*64}, inputs_after={'spec': 'a'*64},
environment={'P23_LAYOUT': 'A'}, environment_after={'P23_LAYOUT': 'A'}, actual_pid=123,
profile='/private/test', profile_name='test', profile_before={'existed': False, 'files': {}}, profile_after={})
for field in ('engine_sha256', 'settings_sha256', 'environment_sha256'):
receipt[field] = receipt[field+'_after'] = 'a'*64
check.inspect_os_receipt(receipt, source)
for field, value in [('engine_ended', False), ('default_after', {'changed': True}),
('source_after', {'source.gd': {'sha256': 'b' * 64}}),
('stage_after', {'changed': True}), ('owned_cleanup', []),
('environment_after', {}), ('profile_before', {}),
('engine_sha256_after', 'b'*64), ('spec_sha256', 'b'*64)]:
with self.subTest(field=field):
altered = copy.deepcopy(receipt)
altered[field] = value
with self.assertRaises(ValueError):
check.inspect_os_receipt(altered, source)
class SemanticTests(unittest.TestCase):
def setUp(self):
self.guest = dict(guest_id='g', state='QUEUING', current_cell=[1, 0, 0],