P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-16 14:17:14 +09:00
parent 9a26f9ab8c
commit 6c13940cf4
10 changed files with 724 additions and 50 deletions
+229 -12
View File
@@ -15,6 +15,7 @@ from pathlib import Path
import re
import sys
import struct
import subprocess
import zlib
sys.dont_write_bytecode = True
@@ -315,7 +316,7 @@ def drawing_rectangle_rows(report):
expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'],
a['at'] + [16*a['scale'], 18*a['scale']], people, None))
def badge(category, identifier, b):
expected.append((category, identifier, b['at'] + [None, 14], world, b['text']))
expected.append((category, identifier, badge_rectangle(b['at'], b['text']), world, b['text']))
for fid, room in report['rooms'].items():
if 'rect' in room:
expected.append(('room', fid, room['rect'], tower, None))
@@ -419,7 +420,7 @@ def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None)
if text is not None:
require(row['text'] == text and type(row['font_width']) in (int, float)
and math.isfinite(row['font_width']) and row['font_width'] > 0, 'offline font/text')
rect[2] = row['font_width'] + 6
require(row['font_width'] <= rect[2] - 6, 'badge text exceeds reproducible rectangle')
close(row['rect'], rect, 'offline report drawing rectangle')
colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text())))
require(row['style_colors'] == colors and colors, 'offline source palette provenance')
@@ -796,6 +797,56 @@ class Transform:
return [x, y, max(r[0] + r[2] for r in rects) - x, max(r[1] + r[3] for r in rects) - y]
def badge_rectangle(at, text):
return list(at) + [len(text) * 10 + 6, 14]
def intersects(a, b):
cut = rect_intersection(a, b)
return cut[2] > 0 and cut[3] > 0
def validate_cabin_intrusion(report):
for cabin in report['cabins']:
require(len(cabin['passengers']) <= cabin['passenger_count'], 'passenger drawn in empty cabin')
for actor in report.get('actor_drawings', []):
require(not intersects(actor_identity_core(actor), cabin['rect']),
'non-passenger inside cabin: ' + actor['key'])
def traffic_obstacles(report):
obstacles = []
for cabin in report['cabins']:
rect = badge_rectangle(cabin['badge_at'], cabin['badge'])
close(cabin['badge_rect'], rect, 'cabin badge rectangle')
obstacles.extend([cabin['rect'], rect])
for collection in ('badges', 'landing_badges', 'actor_aggregations'):
obstacles.extend(badge_rectangle(b['at'], b['text']) for b in report.get(collection, []))
return obstacles
def traffic_segments(band, cell, obstacles):
# Independent rectangle-set subtraction, including the deterministic fallback
# for a band entirely behind a cabin. Never accept self-asserted no-overlap.
for y in (band[1], cell[1], cell[1] + cell[3] - band[3]):
pieces = [[band[0], y, band[2], band[3]]]
for obstacle in obstacles:
remaining = []
for x, top, w, h in pieces:
cut = rect_intersection([x, top, w, h], obstacle)
if not cut[2] or not cut[3]:
remaining.append([x, top, w, h])
continue
left, upper, cw, ch = cut
remaining.extend(r for r in ([x, top, w, upper-top],
[x, upper+ch, w, top+h-upper-ch], [x, upper, left-x, ch],
[left+cw, upper, x+w-left-cw, ch]) if r[2] > 0 and r[3] > 0)
pieces = remaining
if pieces:
return pieces
return []
def compare_traffic(snapshot, report):
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
@@ -810,7 +861,8 @@ def compare_traffic(snapshot, report):
band = [rect[0] + rect[2] * .12, y + h - 2 * (w / 16) - thickness / 2,
rect[2] * .76, thickness]
rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'},
rect=rect, band_rect=band))
rect=rect, band_rect=band,
segment_rects=traffic_segments(band, rect, traffic_obstacles(report))))
close(report['traffic'], rows, 'traffic and cell rect')
validate_life_overlay(report, snapshot=snapshot)
@@ -821,7 +873,7 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
require(type(serial) is int and serial > 0
and type(report.get('overlay_consumed_serial')) is int
and report['overlay_consumed_serial'] == serial, 'stale LifeOverlay serial')
require(report.get('overlay_passes') == ['traffic', 'actors'], 'LifeOverlay pass order')
require(report.get('overlay_passes') == ['traffic', 'cabins', 'actors'], 'LifeOverlay pass order')
indices = report.get('overlay_child_indices', [])
require(len(indices) == 3 and all(type(i) is int and i >= 0 for i in indices)
and indices[0] < indices[1] < indices[2], 'LifeOverlay child order')
@@ -839,8 +891,18 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
require('_draw_congestion_overlay(' not in draw and '_draw_actors(' not in draw
and '_draw_observed_actor(' not in body('_draw_elevators'), 'actor or band drawn in parent')
require(0 <= overlay.find('_draw_congestion_overlay(_life_overlay)')
< overlay.find('_draw_elevators(_life_overlay)')
< overlay.find('_draw_life_badges(_life_overlay)')
< overlay.find('_draw_observed_actor(actor, _life_overlay)')
< overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order')
require('canvas.draw_rect(rect, Color("34413b"))' in body('_draw_readable_badge')
and 'canvas.draw_rect(row[&"band_rect"], Color(color, 0.90))' in body('_draw_congestion_overlay'),
'occluded traffic requires opaque later badges and an actual band draw')
require('text.length() * 10 + 6' in body('_readable_badge_rect')
and '_readable_badge_rect(at, text)' in body('_draw_readable_badge')
and 'PixelArt.elevator_cabin(canvas,' in body('_draw_elevators')
and 'PixelArt.elevator_gate(canvas,' in body('_draw_elevators')
and '_draw_elevators(' not in draw, 'cabin/badge draw geometry or active-layer canvas differs')
actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']]
# Projection authority comes from raw state, never riding/region flags.
cabin_actors, carried_keys = set(), set()
@@ -861,23 +923,34 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
if carried_keys:
# Reuse the original raw binding, track and raised-region fit checks.
compare_actor_geometry(snapshot, report)
obstacles = traffic_obstacles(report)
close(report['traffic_obstacles'], obstacles, 'traffic obstacle geometry')
validate_cabin_intrusion(report)
for row in report.get('traffic', []):
x, y, w, h = row['rect']
thickness = max(2, min(5, h * .16))
expected = [x + w * .12, y + h + 2 - 2 * ((w + 4) / 16) - thickness / 2,
w * .76, thickness]
close(row['band_rect'], expected, 'traffic band rectangle')
segments = traffic_segments(expected, row['rect'], obstacles)
close(row['segment_rects'], segments, 'traffic segment geometry')
require(all(not intersects(part, obstacle) for part in segments for obstacle in obstacles),
'traffic intersects cabin or badge')
if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']:
continue
intervals = []
for actor in actors:
core = actor_identity_core(actor)
intersects = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2]
overlaps = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2]
and core[1] < expected[1] + expected[3] and expected[1] < core[1] + core[3])
require(not intersects or id(actor) in cabin_actors
require(not overlaps or id(actor) in cabin_actors
or (actor in report['actor_drawings'] and actor['key'] in carried_keys),
'floor actor core over traffic band: ' + actor['key'])
if intersects:
# The relocated fragments also cannot cross an ordinary floor core.
require(not any(intersects(core, part) for part in segments)
or id(actor) in cabin_actors or actor['key'] in carried_keys,
'floor actor core over traffic segment: ' + actor['key'])
if overlaps:
intervals.append((max(core[0], expected[0]),
min(core[0] + core[2], expected[0] + expected[2])))
end = expected[0]
@@ -915,7 +988,11 @@ def compare_queues(snapshot, report):
for trip in elevators['trips']:
if trip['state'] == 'QUEUED':
add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from'])
lift_cells = {tuple(cell) for shaft in elevators['shafts'] for cell in shaft['stops']}
footprint = list(dict.fromkeys((cell[0] + dx, cell[1], cell[2])
for shaft in elevators['shafts'] for cell in shaft['stops']
for dx in range(max(int(shaft.get('width', 2)), 1))))
close(report['shaft_footprint'], [list(c) for c in footprint], 'shaft footprint')
lift_cells = set(footprint)
walkable = {tuple(cell) for cell in runtime['world']['walkable_cells']}
owners = {tuple(cell): row['entity_id'] for row in runtime['world']['occupancies'] for cell in row['cells']}
reserved, positions, hidden, badges = set(), {}, {}, []
@@ -926,7 +1003,7 @@ def compare_queues(snapshot, report):
for distance in range(1, 9):
target = (cell[0] + direction * distance, cell[1], cell[2])
facility = facilities.get(owners.get(target))
if target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')):
if target in lift_cells or target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')):
break
lane.append(target)
index = 0
@@ -1207,13 +1284,19 @@ def compare_cabin_geometry(snapshot, report):
badge = '満員 ' + badge
if not shaft['operational']:
badge = '停止 ' + badge
else:
badge = '運転 ' + badge
doors = shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING')
badge += (' 開 ' if doors else ' 閉 ') + shaft['phase']
expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'],
doors_open=shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING'),
phase=shaft['phase'], passenger_count=count, doors_open=doors,
badge_rect=badge_rectangle([rect[0], rect[1]-14], badge),
badge_at=[rect[0], rect[1]-14], badge=badge))
require(len(report['cabins']) == len(expected), 'cabin count')
for row, wanted in zip(report['cabins'], expected):
for key, value in wanted.items():
close(row[key], value, 'cabin/' + key)
validate_cabin_intrusion(report)
def compare_motion(state, report, audit):
@@ -1693,6 +1776,126 @@ def BLOCKERS(executed=None):
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
PLAN = 'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md'
BASE_COMMIT = '860136f7099f5e3aee99da5798a627a77f2d8eea'
ALLOWED_PATHS = ('scenes/main/world_preview.gd', 'scripts/ui/room_presentation.gd',
'tests/ui/test_p23_life_observation.gd', 'tests/ui/test_p23_life_observation.gd.uid',
'tests/fixtures/p23-03/**', 'tools/run_p23_life_observation.py',
'tools/check_p23_life_observation_evidence.py', 'tools/test_p23_life_observation_evidence.py',
'tools/regression/suite-policy.json',
'docs/plans/original-completion-2026-10-02/execution/p23-03/**', CONTRACT)
def strict_json(data):
def pairs(items):
result = {}
for key, value in items:
require(key not in result, 'duplicate JSON key: ' + key)
result[key] = value
return result
return json.loads(data, object_pairs_hook=pairs,
parse_constant=lambda value: require(False, 'nonfinite JSON: ' + value))
def marker_join(base, submitted, main):
"""Preserve M verbatim structurally, adding exactly the S P23 row."""
test = 'tests/ui/test_p23_life_observation.gd'
for document in (base, submitted, main):
rows = document['rows']
require(len({row['test'] for row in rows}) == len(rows), 'duplicate marker test')
additions = [row for row in submitted['rows'] if row['test'] == test]
require(len(additions) == 1 and not any(row['test'] == test for row in base['rows'] + main['rows']),
'marker must add exactly one new P23 row')
stripped = copy.deepcopy(submitted)
stripped['rows'] = [row for row in stripped['rows'] if row['test'] != test]
require(structural_equal(stripped, base), 'submitted marker changes non-P23 structure')
joined = copy.deepcopy(main)
joined['rows'].append(copy.deepcopy(additions[0]))
return joined
def structural_equal(a, b):
return json.dumps(a, sort_keys=True, ensure_ascii=False, allow_nan=False) == json.dumps(
b, sort_keys=True, ensure_ascii=False, allow_nan=False)
def verify_marker_join(base, submitted, main, tested):
expected = marker_join(base, submitted, main)
p23 = expected['rows'][-1]
require(structural_equal([row for row in tested['rows'] if row['test'] == p23['test']], [p23]),
'joined marker P23 row changed or duplicated')
# merge-tree determines the insertion position; deleting the one addition
# must leave M's entire ordered structure, including all P18 rows, intact.
stripped = copy.deepcopy(tested)
stripped['rows'] = [row for row in stripped['rows'] if row['test'] != p23['test']]
require(structural_equal(stripped, main), 'completion marker is not the strict structural join')
def git_bytes(root, *arguments):
"""Root-only adapter. Worker tests inject an in-memory object database."""
process = subprocess.run(['git', '-C', str(root), *arguments], capture_output=True, check=False)
require(process.returncode == 0, 'Git composition failed/conflicted: ' + ' '.join(arguments))
return process.stdout
def verify_composition(root, roles, source_map, inputs, read_git=None):
require(isinstance(roles, dict), 'missing externally bound S/M/T/Q')
names = ('submitted_sha', 'main_context_sha', 'tested_tree', 'tested_commit')
require(all(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]) for k in names),
'invalid externally bound S/M/T/Q')
s, m, t, q = (roles[k] for k in names)
git = read_git or (lambda *args: git_bytes(root, *args))
for commit in (BASE_COMMIT, s, m, q):
require(git('cat-file', '-t', commit).strip() == b'commit', 'role is not a commit')
require(git('cat-file', '-t', t).strip() == b'tree', 'T is not a tree')
require(git('rev-parse', q+'^{tree}').decode().strip() == t, 'Q does not contain T')
require(git('rev-parse', 'HEAD').decode().strip() == q, 'checkout is not Q')
require(not git('status', '--porcelain'), 'tested checkout is dirty')
merged = git('merge-tree', '--write-tree', s, m).decode().splitlines()
require(merged and merged[0] == t, 'T differs from deterministic merge-tree')
def tree(revision):
result = {}
for record in git('ls-tree', '-r', '-z', revision).split(b'\0'):
if not record:
continue
metadata, path = record.split(b'\t', 1)
mode, kind, oid = metadata.decode().split()
result[path.decode()] = (mode, kind, oid)
return result
base, submitted, main, tested = map(tree, (BASE_COMMIT, s, m, t))
changed_s = {p for p in base.keys() | submitted.keys() if base.get(p) != submitted.get(p)}
changed_m = {p for p in base.keys() | main.keys() if base.get(p) != main.get(p)}
def allowed(path):
return any(path.startswith(rule[:-2]) if rule.endswith('/**') else path == rule
for rule in ALLOWED_PATHS)
require(all(allowed(p) for p in changed_s), 'S changed a path outside allowed_paths')
for paths, authority, label in ((changed_s-changed_m, submitted, 'S-only'),
(changed_m-changed_s, main, 'M-only')):
for path in paths:
require(tested.get(path) == authority.get(path), label + ' blob/mode provenance: ' + path)
def document(revision):
return strict_json(git('show', revision + ':' + CONTRACT))
verify_marker_join(document(BASE_COMMIT), document(s), document(m), document(t))
source_names = {p for p in tested if p.split('/', 1)[0] in
('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools')
and Path(p).name != '.DS_Store' and '__pycache__' not in Path(p).parts}
source_names.update(('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md'))
require(set(source_map) == source_names, 'incomplete tested-tree source/settings/QA map')
# Source refs include all QA suites and settings, not just S-changed files.
required = set(source_map) | {p.split('#', 1)[0] for p in inputs} | {PLAN, CONTRACT,
'project.godot', 'tests/business/test_required_kitchen.gd', 'tools/regression/suite-policy.json'}
bindings = roles.get('blob_bindings')
require(isinstance(bindings, dict) and set(bindings) == required, 'incomplete/extraneous T blob bindings')
for path in sorted(required):
entry = tested.get(path)
require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path)
content = git('show', t + ':' + path)
require((root/path).read_bytes() == content, 'checkout bytes differ from T: ' + path)
digest = hashlib.sha256(content).hexdigest()
if path in source_map:
require(source_map[path] == digest, 'evidence source is not T: ' + path)
if path in inputs:
require(inputs[path] == digest, 'evidence input is not T: ' + path)
return {k: roles[k] for k in names}
def normative_plan_text(root):
@@ -1780,6 +1983,11 @@ def inspect_resume(writer, reader, saved, requests, formal_members):
return ['distinct_pid', 'formal_members', 'load_ack']
def require_screenshot_coverage(action, screenshots):
require(isinstance(screenshots, list) and len(screenshots) >= (1 if action == 'screenshot' else 2),
'screenshot-only needs one image; real input needs before/after images')
def inspect_os_requests(directory, layout, role, pid, root=ROOT):
pair = directory/'os-pair'/layout
def path(name):
@@ -1801,6 +2009,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
observed = json.loads(ack_path.with_name('observed.json').read_bytes())
request, ack = json.loads(request_path.read_bytes()), json.loads(ack_path.read_bytes())
cua = document(ack['input_receipt'])
require_screenshot_coverage(request['action'], cua.get('screenshots'))
require(request['sequence'] == ack['sequence'] == observed['sequence'] == sequence, 'OS request sequence')
require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == pid, 'OS request PID')
digest = sha(request_path)
@@ -1887,7 +2096,7 @@ def inspect_native_phases(directory, index, root=ROOT):
and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker')
def check(root, directory):
def check(root, directory, root_roles=None):
"""Never infer completion of unimplemented gates from an index assertion."""
deficits = []
executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS}
@@ -1897,8 +2106,13 @@ def check(root, directory):
return ['missing evidence index: ' + str(index_path)]
try:
index = json.loads(index_path.read_bytes())
require(index.get('schema') == 'p23-life-observation-v2',
'historical v1 evidence has diagnostic applicability only')
runner = module('p23_source_applicability', root/'tools/run_all_tests.py')
require(index.get('source_map') == {n: v['sha256'] for n, v in runner.source_refs(root).items()}, 'exact frozen source map')
roles = verify_composition(root, root_roles, index['source_map'], input_map(root))
require(index.get('composition') == roles and index.get('commit') == roles['tested_commit'],
'evidence role binding differs from Root S/M/T/Q; historical v1 is not T evidence')
if index.get('input_map') != input_map(root):
deficits.append('normative input_map differs from current immutable inputs')
if index.get('spec_sha256') != SPEC_SHA:
@@ -1985,6 +2199,8 @@ def main(argv=None):
parser.add_argument('--offline-mirror', type=Path)
parser.add_argument('--layout-a', type=Path)
parser.add_argument('--layout-b', type=Path)
parser.add_argument('--root-composition', type=Path,
default=os.environ.get('P23_ROOT_COMPOSITION'))
args = parser.parse_args(argv)
check_setup_budget_source((args.project_root / 'tests/fixtures/p23-03/observation.gd').read_text())
if args.offline_mirror:
@@ -1997,7 +2213,8 @@ def main(argv=None):
if not build:
print('BUILD_OUTPUT_DIR is required', file=sys.stderr)
return 2
deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence')
root_roles = strict_json(args.root_composition.read_bytes()) if args.root_composition else None
deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence', root_roles)
print(json.dumps(dict(task_id='P23-03', integrity='fail' if deficits else 'pass',
deficits=deficits, acceptance='pending'), ensure_ascii=False))
return 1 if deficits else 0