P23-03: 生活の観察

This commit is contained in:
Agent Control Runner
2026-10-16 19:18:11 +09:00
parent 26f60095bf
commit 059c2f743b
6 changed files with 285 additions and 4 deletions
@@ -3707,7 +3707,7 @@
"template": "P23 life observation: <checks> checks, <failures> failures",
"required": true,
"fixed_check_count_used": false,
"source_sha256_at_schema_audit": "5c52db4dd70f9702c096e918d849703d14aec1acb88bb2458ca01049da71ce3c",
"source_sha256_at_schema_audit": "750254af7d4b8b00508e25d1ef744837d3a7d14ddebe12398e8014bf5e8c68e8",
"authority": {
"path": "docs/spec/acceptance/P23-03.json",
"scope": "component and read-only observation only; native unexecuted; whole unit blocked"
@@ -0,0 +1,69 @@
# Repair36 worker submission
Scope: world_preview observation labels, independent checker, Python/GD controls.
No Runtime, service, data, art, policy, original negative, or acceptance changes.
Input authority: registered BUILD `audit-support/badge-repair36-exact-inputs/EXACT_INPUTS.json`, source 26f60095; original paths are provenance identifiers.
The resolver places fixed cabin rectangles/badges first, then business, supply,
landing, aggregation in array order. A positive-area collision advances to the
first blocking rectangle's right edge and repeats. With finite rectangles of
positive width this always terminates on the anchor row; the specified upper
and lower fallback rows are therefore unreachable. No margin, pixel threshold,
clipping, truncation, member alteration, or new viewport rule is introduced.
Original anchors and ordered blocker identities are retained in deep-copy reports.
Traffic obstacles use the resolved positions; actor placement/traffic subtraction
and cabin/transit guards retain their existing implementations.
The checker separately projects anchors back into the original report shape for
existing raw-state queue/business/aggregation recomputation. It independently
replays label geometry and validates reported positions and blocker identities.
All label pairs must have zero positive-area intersection. Both production image
review consumers require explicit indexed `judgments`; the legacy `rows` helper
interface remains available for unchanged historical unit controls only. Compact
bits are never accepted for new image-review evidence.
## Static all40 comparison
Inputs: all40 original manifests plus their unchanged original requests in
`p23-normal-os-26f-r17`, read only. This is a counterfactual static projection,
not a new rendered screenshot or native/OS acceptance.
Original manifest label pairs: exactly one intersection, B/writer-request-0021,
rows 5 (queue) and 26 (aggregation). Resolved labels: zero intersections in all
40 frames, including fixed cabin labels. Thirty frames have no movement.
The following is the complete movement list in local canvas coordinates:
| Frame | Label | Original | Resolved | displaced_by |
| --- | --- | --- | --- | --- |
| A/reader-request-0003 | landing/0 | 563,468 | 755,468 | cabin/0/rect |
| A/writer-request-0014 | landing/0 | 563,468 | 845,468 | cabin/0/badge |
| A/writer-request-0021 | landing/0 | 563,468 | 875,468 | cabin/0/badge |
| A/writer-request-0025 | landing/0 | 563,468 | 855,468 | cabin/0/badge |
| A/writer-request-0030 | landing/0 | 563,468 | 755,468 | cabin/0/rect |
| B/reader-request-0003 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge |
| B/writer-request-0014 | landing/0 | 563,468 | 1101,468 | cabin/0/badge, cabin/1/badge |
| B/writer-request-0014 | landing/1 | 819,468 | 1217,468 | cabin/1/badge, landing/0 |
| B/writer-request-0021 | landing/0 | 563,468 | 755,468 | cabin/0/rect |
| B/writer-request-0021 | landing/1 | 819,468 | 1011,468 | cabin/1/rect |
| B/writer-request-0021 | aggregation/0 | 563,372 | 825,372 | landing/2, cabin/0/badge |
| B/writer-request-0025 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge |
| B/writer-request-0030 | landing/1 | 819,468 | 1111,468 | cabin/0/badge, cabin/1/badge |
The affected queue anchor remains local [563,372], corresponding to original
manifest screenshot [642,469]. Its five members remain hidden and its complete
label remains unchanged. The historical reviewer index24/text26 disagreement
and all prior negatives remain untouched; no staff defect or acceptance credit
is inferred.
## Validation boundary
Worker runs Python syntax/local semantic tests only. GD controls are added but
not executed here. Root still must replay currentbad, run localized/native,
new normal OS A/B with indexed independent image review, then one full regression
and formal review/integration/ledger in the recorded order. No formal acceptance,
original88 credit, Windows, held-gesture, performance, or asset credit is claimed.
Local result: Python AST syntax PASS; 146 Python tests PASS (142 retained plus
four focused controls). All 142 pre-existing test method ASTs match HEAD exactly.
`git diff --check` PASS. Only the existing P23 GD marker source SHA was refreshed;
no marker requirements or policies changed. No Godot/native/OS/full was run.
+34
View File
@@ -637,6 +637,7 @@ func _build_life_observation() -> Dictionary:
&"at": rect.position + Vector2(2, 16), &"requests": requests,
&"text": RoomPresentation.supply_label(requests)})
model[&"badges"] = badges
_resolve_observation_labels(model)
var obstacles: Array[Rect2] = []
for cabin: Dictionary in model[&"cabins"]:
obstacles.append(cabin[&"rect"])
@@ -691,6 +692,39 @@ func _traffic_segments(band: Rect2, cell: Rect2, obstacles: Array[Rect2]) -> Arr
return []
func _resolve_observation_labels(model: Dictionary) -> void:
var placed: Array[Dictionary] = []
for i: int in model[&"cabins"].size():
var cabin: Dictionary = model[&"cabins"][i]
placed.append({&"id": "cabin/%d/rect" % i, &"rect": cabin[&"rect"]})
placed.append({&"id": "cabin/%d/badge" % i, &"rect": cabin[&"badge_rect"]})
for kind: String in ["business", "supply", "landing", "aggregation"]:
var collection: Array = model[&"badges"] if kind in ["business", "supply"] else model[&"landing_badges"] if kind == "landing" else model.get(&"actor_aggregations", [])
for i: int in collection.size():
var badge: Dictionary = collection[i]
if kind in ["business", "supply"] and badge[&"kind"] != kind:
continue
var anchor: Vector2 = badge[&"at"]
var at := anchor
var displaced_by: Array[String] = []
# Each collision advances to an existing right edge. Positive widths
# guarantee termination; touching edges are allowed.
while true:
var blocker: Dictionary = {}
for previous: Dictionary in placed:
if _readable_badge_rect(at, badge[&"text"]).intersection(previous[&"rect"]).has_area():
blocker = previous
break
if blocker.is_empty():
break
displaced_by.append(blocker[&"id"])
at.x = (blocker[&"rect"] as Rect2).end.x
badge[&"anchor"] = anchor
badge[&"at"] = at
badge[&"displaced_by"] = displaced_by.duplicate(true)
placed.append({&"id": "%s/%d" % [kind, i], &"rect": _readable_badge_rect(at, badge[&"text"])})
func _draw_life_badges(canvas: CanvasItem) -> void:
for badge: Dictionary in _life_observation.get(&"badges", []):
_draw_readable_badge(badge[&"at"], badge[&"text"], canvas)
+26
View File
@@ -100,6 +100,31 @@ func repair28_geometry() -> void:
func _initialize() -> void:
call_deferred("run")
func repair36_labels() -> void:
var view := WorldPreview.new()
var members: Array[String] = ["guest/a", "guest/b", "guest/c"]
var identities: Array[Dictionary] = []
for key: String in members:
identities.append({&"key": key, &"id": key.trim_prefix("guest/"), &"state": 0, &"cell": Vector3i.ZERO, &"mode": &"idle"})
var queue := {&"at": Vector2(40,40), &"text": "待ち3人(他3人)", &"cell": Vector3i.ZERO, &"members": members.duplicate(), &"visible_count": 0, &"overflow": members.size()}
var aggregate := {&"at": queue[&"at"], &"text": "+3", &"members": identities.duplicate(true), &"drawn_keys": []}
var model := {&"cabins": [], &"badges": [], &"landing_badges": [queue], &"actor_aggregations": [aggregate]}
var blocked := Rect2(100,100,20,20)
model[&"cabins"] = [{&"rect": blocked, &"badge_at": Vector2(200,200), &"badge": "EV", &"badge_rect": view._readable_badge_rect(Vector2(200,200), "EV")}]
model[&"shaft_footprint"] = []
var region: Dictionary = view._life_landing_region({&"bounds": blocked, &"transit": false}, Vector3i.ZERO, model)
expect(region.get(&"no_lane", false) and region[&"badge_at"] == queue[&"at"], "lane-less stranded actor uses hidden queue anchor")
view._resolve_observation_labels(model)
expect(not view._readable_badge_rect(queue[&"at"], queue[&"text"]).intersection(view._readable_badge_rect(aggregate[&"at"], aggregate[&"text"])).has_area(), "hidden queue and stranded aggregate labels do not overlap")
expect(queue[&"text"] == "待ち3人(他3人)" and aggregate[&"text"] == "+3", "complete counts remain readable")
expect(queue[&"members"] == members and aggregate[&"members"] == identities and aggregate[&"drawn_keys"].is_empty(), "label placement preserves member identities and visibility")
expect(queue[&"at"] == queue[&"anchor"] and aggregate[&"anchor"] == queue[&"anchor"], "uncontested queue keeps anchor and stranded anchor remains recorded")
var solitary := {&"at": Vector2(400,400), &"text": "+1"}
model[&"actor_aggregations"] = [solitary]
view._resolve_observation_labels(model)
expect(solitary[&"at"] == solitary[&"anchor"] and solitary[&"displaced_by"].is_empty(), "uncontested label stays at anchor")
view.free()
func expect(value: bool, message: String) -> void:
checks += 1
if not value:
@@ -118,6 +143,7 @@ func run() -> void:
var clock: Variant = root.get_node("GameClock")
clock.set_paused(true)
repair28_geometry()
repair36_labels()
# Isolated real typed components test state labels, without claiming gameplay.
var labels: Dictionary = {}
for state: int in FacilityInstance.State.values():
+74 -3
View File
@@ -310,6 +310,9 @@ def paired_crop_noise(before, after, rect):
def drawing_rectangle_rows(report):
"""Derive identities and local positions; do not read audit verdicts."""
if any('anchor' in b for key in ('badges', 'landing_badges', 'actor_aggregations')
for b in report.get(key, [])):
validate_observation_labels(report)
expected = []
tower = 'scripts/ui/pixel_tower_art.gd'
people = 'scripts/ui/pixel_people.gd'
@@ -549,13 +552,16 @@ def write_frame_review(directory, frame, provenance):
return manifest, digest
def reviewed_pixel_categories(manifest, digest, verdict_path):
def reviewed_pixel_categories(manifest, digest, verdict_path, *, require_judgments=False):
"""The worker never writes verdicts; absent independent review earns nothing."""
if not Path(verdict_path).is_file():
return set()
verdict = json.loads(Path(verdict_path).read_bytes())
require(verdict['manifest_sha256'] == digest, 'review manifest SHA mismatch')
require(verdict['reviewer'] == 'independent-opus-direct-image', 'independent image reviewer required')
if require_judgments:
require(isinstance(verdict.get('judgments'), list), 'explicit indexed judgments required')
verdict = dict(verdict, rows=verdict['judgments'])
eligible = [r for r in manifest['rows'] if r['eligibility'] != 'outside']
require([v['row_index'] for v in verdict['rows']] == [r['row_index'] for r in eligible],
'review row coverage')
@@ -651,7 +657,7 @@ def inspect_offline_mirror(directory):
receipt_sha256=sha(folder/'cua-receipt.json')))
review_dir = ROOT/'docs/plans/original-completion-2026-10-02/execution/p23-03/offline-r07-review'/name
manifest, digest = write_frame_review(review_dir, frame, provenance)
credited = reviewed_pixel_categories(manifest, digest, review_dir/'independent-verdict.json')
credited = reviewed_pixel_categories(manifest, digest, review_dir/'independent-verdict.json', require_judgments=True)
present = {r['category'] for r in frame['rows'] if r['eligibility'] != 'outside'}
overlays = [dict(cell=r['cell'], rect=r['rect']) for r in report['traffic']
if (r['traffic']['current_density'] > 0 or r['traffic']['temporary_block'])
@@ -803,6 +809,61 @@ def badge_rectangle(at, text):
return list(at) + [len(text) * 10 + 6, 14]
def observation_label_layout(report):
"""Independent ordered rectangle placement; never consume reported at/reasons."""
placed, result = [], []
for i, cabin in enumerate(report['cabins']):
placed.extend([(f'cabin/{i}/rect', cabin['rect']),
(f'cabin/{i}/badge', badge_rectangle(cabin['badge_at'], cabin['badge']))])
for kind, collection in [('business', 'badges'), ('supply', 'badges'),
('landing', 'landing_badges'), ('aggregation', 'actor_aggregations')]:
for i, row in enumerate(report.get(collection, [])):
if collection == 'badges' and row['kind'] != kind:
continue
anchor = list(row.get('anchor', row['at']))
at, reasons = anchor[:], []
while True:
conflict = next(((identity, rect) for identity, rect in placed
if intersects(badge_rectangle(at, row['text']), rect)), None)
if conflict is None:
break
identity, rect = conflict
reasons.append(identity)
at[0] = rect[0] + rect[2]
result.append((collection, i, dict(anchor=anchor, at=at, displaced_by=reasons)))
placed.append((f'{kind}/{i}', badge_rectangle(at, row['text'])))
return result
def observation_anchor_report(report):
"""Projection for existing raw-state semantic recomputation, not evidence."""
import copy
raw = copy.deepcopy(report)
for collection in ('badges', 'landing_badges', 'actor_aggregations'):
for row in raw.get(collection, []):
if 'anchor' in row:
row['at'] = row.pop('anchor')
row.pop('displaced_by', None)
return raw
def validate_observation_labels(report):
require(all(key in report for key in ('cabins', 'badges', 'landing_badges', 'actor_aggregations')),
'missing observation label collections')
rectangles = [badge_rectangle(c['badge_at'], c['badge']) for c in report['cabins']]
rectangles += [badge_rectangle(b['at'], b['text'])
for key in ('badges', 'landing_badges', 'actor_aggregations')
for b in report.get(key, [])]
for i, rect in enumerate(rectangles):
require(not any(intersects(rect, other) for other in rectangles[:i]),
'observation label intersection')
for collection, index, wanted in observation_label_layout(report):
row = report[collection][index]
for key, value in wanted.items():
require(key in row, 'missing label ' + key)
close(row[key], value, 'observation label/' + key)
def intersects(a, b):
cut = rect_intersection(a, b)
return cut[2] > 0 and cut[3] > 0
@@ -965,6 +1026,9 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
def compare_queues(snapshot, report):
if any('anchor' in b for b in report.get('landing_badges', [])):
validate_observation_labels(report)
report = observation_anchor_report(report)
runtime = snapshot['systems']['runtime']
transform = Transform(runtime, report)
facilities = codec.indexed(runtime['facilities']['instances'], 'instance_id')
@@ -1222,6 +1286,9 @@ def compare_goods(snapshot, report, root=ROOT):
def compare_badges(state, report):
if any('anchor' in b for b in report.get('badges', [])):
validate_observation_labels(report)
report = observation_anchor_report(report)
runtime = state['systems']['runtime']
transform = Transform(runtime, report)
expected = []
@@ -1490,6 +1557,9 @@ def landing_actor_region(state, report, transform, cell, region, transit):
def compare_actor_geometry(state, report):
if any('anchor' in b for b in report.get('actor_aggregations', [])):
validate_observation_labels(report)
report = observation_anchor_report(report)
runtime = state['systems']['runtime']
compare_queues(state, report)
compare_room_services(state, report)
@@ -1800,6 +1870,7 @@ def inspect_stream(directory, root=ROOT):
report = read_object(directory, row['report'])
fields = compare_actor_rows(state, report, root)
require(report['elevators'] == elevator_report(state), 'typed elevator report mismatch')
validate_observation_labels(report)
compare_room_services(state, report, root)
compare_rest(state, report, read_object(directory, row['rest_audit']), root)
compare_queues(state, report)
@@ -2240,7 +2311,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
require(frame['mechanical_pass'], 'frame mechanical checks: ' + str(frame['errors'])
+ str([r['failures'] for r in frame['rows'] if not r['mechanical_pass']]))
pixel_categories.update(reviewed_pixel_categories(
manifest, manifest_sha, review_dir/'independent-verdict.json'))
manifest, manifest_sha, review_dir/'independent-verdict.json', require_judgments=True))
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
@@ -2293,5 +2293,86 @@ class FrameReviewTests(unittest.TestCase):
self.assertEqual(check.reviewed_pixel_categories(manifest, digest, verdict_path), set())
class Repair36LabelTests(unittest.TestCase):
inputs = check.ROOT.parent / '.runtime/P23-03/build/audit-support/badge-repair36-exact-inputs'
def recorded(self):
request = json.loads((self.inputs/'bad-native-request/request.json').read_bytes())
stream = self.inputs.parent.parent/'p23-normal-os-26f-r17/os-pair/B/writer-stream'
capture = next(json.loads(line) for line in (stream/'manifest.jsonl').read_text().splitlines()
if json.loads(line).get('label') == 'writer-request-0021')
return request, check.read_object(stream, capture['state']), check.decode(request['report'])
def resolved(self, report):
result = copy.deepcopy(report)
for collection, index, fields in check.observation_label_layout(report):
result[collection][index].update(fields)
return result
def test_recorded_negative_and_positive_with_raw_semantics(self):
request, state, old = self.recorded()
rectangles = check.drawing_rectangle_rows(old)
self.assertTrue(check.intersects(rectangles[5][2], rectangles[26][2]))
with self.assertRaises(ValueError):
check.validate_observation_labels(old)
report = self.resolved(old)
check.validate_observation_labels(report)
check.compare_queues(state, report)
check.compare_badges(state, report)
check.compare_actor_geometry(state, report)
queue = next(b for b in report['landing_badges'] if b['visible_count'] == 0)
manifest = json.loads((self.inputs/'bad-native-request/review-image-0/manifest.json').read_bytes())
original = manifest['rows'][5]
self.assertEqual(queue['anchor'], original['local_rect'][:2])
self.assertEqual(original['screenshot_rect'][:2], [642,469])
self.assertEqual(queue['members'], next(b['members'] for b in old['landing_badges'] if b['visible_count'] == 0))
def test_recorded_forgery_controls(self):
_, state, old = self.recorded()
good = self.resolved(old)
for attack in ('at', 'displaced_by', 'count', 'hidden', 'anchor'):
report = copy.deepcopy(good)
row = report['actor_aggregations'][0]
if attack == 'at': row['at'][0] += 1
elif attack == 'displaced_by': row['displaced_by'] = []
elif attack == 'count': row['text'] = '+'
elif attack == 'hidden': report['actor_aggregations'] = []
else: row['anchor'][0] += 1
with self.subTest(attack=attack), self.assertRaises(ValueError):
check.validate_observation_labels(report)
check.compare_queues(state, report)
check.compare_actor_geometry(state, report)
def test_generic_order_and_edge_contact(self):
for kinds in [('business', 'aggregation'), ('supply', 'landing'),
('business', 'supply', 'landing', 'aggregation')]:
report = dict(cabins=[], badges=[], landing_badges=[], actor_aggregations=[])
for kind in kinds:
key = 'badges' if kind in ('business', 'supply') else 'landing_badges' if kind == 'landing' else 'actor_aggregations'
report[key].append(dict(kind=kind, at=[10,20], text='abc'))
result = self.resolved(report)
check.validate_observation_labels(result)
positions = [fields['at'] for _, _, fields in check.observation_label_layout(result)]
self.assertEqual(positions, [[10+36*i,20] for i in range(len(kinds))])
report = dict(cabins=[dict(rect=[0,50,20,20], badge_at=[10,20], badge='abc')],
badges=[], landing_badges=[dict(at=[10,20], text='x')], actor_aggregations=[])
self.assertEqual(self.resolved(report)['landing_badges'][0]['at'], [46,20])
report['landing_badges'][0]['at'] = [46,20]
self.assertEqual(self.resolved(report)['landing_badges'][0]['displaced_by'], [])
def test_future_review_requires_indexed_judgments(self):
manifest = dict(rows=[dict(row_index=7, category='queue', eligibility='visible', mechanical_pass=True)], mechanical_pass=True)
verdict = dict(manifest_sha256='digest', reviewer='independent-opus-direct-image',
state_bits='1', position_bits='1', category_bits='1')
with tempfile.TemporaryDirectory() as folder:
path = Path(folder)/'verdict.json'
path.write_text(json.dumps(verdict))
with self.assertRaisesRegex(ValueError, 'indexed judgments'):
check.reviewed_pixel_categories(manifest, 'digest', path, require_judgments=True)
verdict['judgments'] = [dict(row_index=7, category=True, state=True, position=True)]
path.write_text(json.dumps(verdict))
self.assertEqual(check.reviewed_pixel_categories(manifest, 'digest', path, require_judgments=True), {'queue'})
if __name__ == '__main__':
unittest.main()