P23-03: 生活の観察
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user