From 059c2f743be655c11627a2b19d8eab9f86a000a6 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Fri, 16 Oct 2026 19:18:11 +0900 Subject: [PATCH] =?UTF-8?q?P23-03:=20=E7=94=9F=E6=B4=BB=E3=81=AE=E8=A6=B3?= =?UTF-8?q?=E5=AF=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../completion_marker_contracts.json | 2 +- .../execution/p23-03/REPAIR36_LOCAL_AUDIT.md | 69 ++++++++++++++++ scenes/main/world_preview.gd | 34 ++++++++ tests/ui/test_p23_life_observation.gd | 26 ++++++ tools/check_p23_life_observation_evidence.py | 77 +++++++++++++++++- tools/test_p23_life_observation_evidence.py | 81 +++++++++++++++++++ 6 files changed, 285 insertions(+), 4 deletions(-) create mode 100644 docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md diff --git a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json index 2f7fe65..8820fb1 100644 --- a/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json +++ b/docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json @@ -3707,7 +3707,7 @@ "template": "P23 life observation: checks, 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" diff --git a/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md new file mode 100644 index 0000000..a5d2993 --- /dev/null +++ b/docs/plans/original-completion-2026-10-02/execution/p23-03/REPAIR36_LOCAL_AUDIT.md @@ -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. diff --git a/scenes/main/world_preview.gd b/scenes/main/world_preview.gd index bac0f27..fa32fe0 100644 --- a/scenes/main/world_preview.gd +++ b/scenes/main/world_preview.gd @@ -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) diff --git a/tests/ui/test_p23_life_observation.gd b/tests/ui/test_p23_life_observation.gd index fd56e60..6cf5c50 100644 --- a/tests/ui/test_p23_life_observation.gd +++ b/tests/ui/test_p23_life_observation.gd @@ -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(): diff --git a/tools/check_p23_life_observation_evidence.py b/tools/check_p23_life_observation_evidence.py index 95b99f0..8a62d1e 100644 --- a/tools/check_p23_life_observation_evidence.py +++ b/tools/check_p23_life_observation_evidence.py @@ -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 diff --git a/tools/test_p23_life_observation_evidence.py b/tools/test_p23_life_observation_evidence.py index 68c294b..2b8b1f1 100644 --- a/tools/test_p23_life_observation_evidence.py +++ b/tools/test_p23_life_observation_evidence.py @@ -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()