From f5f85ae08c1c6c9c1ec511f13a7215170d7d8d49 Mon Sep 17 00:00:00 2001 From: Agent Control Runner Date: Thu, 15 Oct 2026 00:12:44 +0900 Subject: [PATCH] =?UTF-8?q?P08-03:=20=E9=85=8D=E7=BD=AE=E3=81=AE=E9=80=9A?= =?UTF-8?q?=E5=B8=B8=E6=93=8D=E4=BD=9C=E5=8F=97=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../execution/p08-03/CONDITION_MATRIX.md | 58 +++++++++++ .../execution/p08-03/implementation.json | 83 ++++++++++++++++ tests/fixtures/p08-03/checker_fixture.py | 23 ++++- tests/fixtures/p08-03/passive_placement.gd | 96 +++++++++++++++++-- tools/check_p08_normal_placement_evidence.py | 36 +++++++ tools/test_p08_normal_placement_evidence.py | 54 +++++++++++ 6 files changed, 342 insertions(+), 8 deletions(-) diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md b/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md index a2cd7ee..8faabf5 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md @@ -501,6 +501,64 @@ A docs/plans/original-completion-2026-10-02/execution/p08-03/runtime-preview-pro ``` Exact validation commands and initial/retry results are recorded in implementation.json repair17.validation. No Godot/native/full execution or recollection was performed. +## Repair19 — submitted, no acceptance + +Input M: fd643f55a271e63c554b94681454d357117c0678. Verify-only failure: +8ee0765cdd0a438dbf0ddc0f67ebbc70. New HEAD: **to be bound by Runner**. +The R15 diagnosis was an unverified palette reveal followed by a possibly clipped +button click; deferred quit also allowed requests and secondary failures after +an ACK timeout or identity/hash failure. The Root31 ACK is invalid. R15 earns no +credit. No acceptance or changed-source evidence reuse is claimed. + +Repair19 changes only the passive endpoint, additive checker gates, synthetic +positive/negative specimens and these handoff records. Repair18 and all R09 pins +remain history without rewriting. Runtime, product UI, the ACK deadline, spec, +scope and denominator remain unchanged. + +### Source-map applicability + +The four changed executable/source inputs and their SHA256 at M and in the +worktree are listed in implementation.json `repair19.source_map`. The two changed +handoff documents are also listed there at M; their final byte SHA256 are reported +with submission, outside the documents to avoid a self-referential hash. + +Read-only rg and preload/load tracing found these consumers: + +- docs recorder `passive_os_placement_recorder.gd:2` preloads passive_placement.gd, + which extends placement.gd. Its `run` drives the writer/reader OS pair. +- `tests/ui/test_p08_normal_placement_ops.gd:2` preloads placement.gd directly. + That parent is unchanged by repair19; it loads main.tscn and the existing codec + and audio cleanup helpers. The passive subclass is not loaded by this UI suite. +- `tools/run_p08_normal_placement.py:145,210,227` loads the checker for initial + input fingerprinting, OS input mapping and final evidence verification. Its + import/parse stages precede handler writer/reader stages using the UI suite, + and its OS stage uses the docs recorder. +- `tools/test_p08_normal_placement_evidence.py:28-30,55-56` loads the checker, + checker_fixture.py and collector, and reads both GDScript fixture sources. +- `tools/run_all_tests.py:roster` discovers the UI suite; `source_refs` includes + tests and tools in the whole-source fingerprint. Import/parse/native and final + regression applicability therefore requires the actual complete fingerprint. + The checker also imports the prior typed codec and standard runner directly. + +Every OS/native collection and regression whose inputs include a changed file +is not applicable and must rerun on the final frozen revision: the complete +p08-normal-placement OS/native pair, checker tests, the UI suite when its actual +input fingerprint includes a changed fixture, and exactly one final all-suite +regression on the roster Root derives at the final revision. No roster count is +assumed. A prior native/import stage may be reused only on an exact fingerprint +match; no reuse is claimed where source changed. The UI suite is included in the +fresh pair and final roster regardless of its unchanged direct parent fixture. + +Neither completion_marker_contracts.json nor suite-policy.json changed: no pinned +print line, suite source SHA or cleanup policy changed in repair19. + +Still pending: Root first inspects normal scrollbar operation in a short disposable +diagnostic, then freezes source and runs a fresh contiguous long OS/native attempt; +exactly one final all-suite regression on the derived roster; independent Opus +review of all five conditions with raw-proof access; integration, current final +verification and ledger. Windows same-distribution/device remains open with no +Mac substitution. Local command outcomes are recorded in implementation.json. + ## Repair18 — submitted, no acceptance 7297a4c050f0293df331c2c51bb32def248bcb6b diff --git a/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json b/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json index 2d7dd5b..df3e89e 100644 --- a/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json +++ b/docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json @@ -1,5 +1,88 @@ { "task_id": "P08-03", + "repair19": { + "input_M": "fd643f55a271e63c554b94681454d357117c0678", + "verify_only_failure": "8ee0765cdd0a438dbf0ddc0f67ebbc70", + "new_HEAD": "to be bound by Runner", + "status": "submitted", + "acceptance": "not claimed; all five conditions pending", + "diagnosis": "R15 reveal published no geometry and verified neither visibility nor scroll direction; deferred quit allowed subsequent requests and secondary failures after a terminal ACK error.", + "owner_rulings": ["Root31 ACK is invalid", "R15 earns no credit"], + "source_map": { + "tests/fixtures/p08-03/passive_placement.gd": { + "at_M": "cdbd9a09b535d00a79aec7e2b7219f4daa4cf235449602b3cd02c6f12d9df88c", + "worktree": "210c95c4df9ac16197ebaad67f7531d66b841e9d03c05c014a846bb9186bc2d0" + }, + "tests/fixtures/p08-03/checker_fixture.py": { + "at_M": "a9d376f0af35814b948347383edf11d299f2e78c3347161a49ae60e5a654ec24", + "worktree": "f80956c487cf8bd9e169011788245c41017447de517c3839685af0daaf7be036" + }, + "tools/check_p08_normal_placement_evidence.py": { + "at_M": "926418042d4ae8c2afba2bd91799f5d32eac62c673ae83c332f3b2d12fcff90d", + "worktree": "bc6bc7109bc754ad8f9c46e6492e044264226545a3c9e823a519ab9f0dd23c88" + }, + "tools/test_p08_normal_placement_evidence.py": { + "at_M": "b83f3c7d917d47d53007b25cb31f11011e2184f53ace3423f38a3007762b5675", + "worktree": "f0de90ad22194df731d90cff24e69860db370aaf768cd6faa5fda4d50e5d0903" + }, + "docs/plans/original-completion-2026-10-02/execution/p08-03/CONDITION_MATRIX.md": { + "at_M": "fb1916827e7f5d4fb40cdb86ba6e282cefa5b7aee6600d591cd7b1d0b37b11f8", + "worktree": "final byte SHA256 reported in submission to avoid self-reference" + }, + "docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json": { + "at_M": "6a594346c17f1309c7471ce357f182203b5f2212586b6b111fc0833526379f9b", + "worktree": "final byte SHA256 reported in submission to avoid self-reference" + } + }, + "applicability": { + "trace": [ + "docs passive_os_placement_recorder.gd:2 -> passive_placement.gd -> placement.gd -> main.tscn; OS writer/reader", + "tests/ui/test_p08_normal_placement_ops.gd:2 -> unchanged placement.gd, cleanup and codec; no direct passive load", + "tools/run_p08_normal_placement.py:145,210,227 -> checker; initial inputs, OS mapping, final verification; import/parse and handler writer/reader use the UI suite, OS uses docs recorder", + "tools/test_p08_normal_placement_evidence.py:28-30,55-56 -> checker, checker_fixture, collector, both fixture sources", + "tools/run_all_tests.py roster discovers UI suite; source_refs fingerprints tests and tools; checker imports prior typed codec and runner" + ], + "rule": "Every OS/native collection and regression including changed inputs is not applicable and must rerun on the final frozen revision. Rerun p08-normal-placement OS/native pair, checker tests, UI suite if its inputs include a changed fixture, and exactly one final all-suite regression on Root-derived final roster. UI is included in fresh pair and final roster even though its direct fixture is unchanged in repair19.", + "native_import_reuse": "Only on exact actual fingerprint match; no changed-source reuse claimed", + "completion_marker_contracts_changed": false, + "suite_policy_changed": false, + "pin_reason": "No pinned print line, suite source SHA or cleanup policy changed" + }, + "validation": [ + { + "command": "python3 -B tools/test_p08_normal_placement_evidence.py", + "result": "initial exit 1; 142 tests in 119.802s. Final executable-source rerun exit 1; 142 tests in 114.501s. Both: 141 passed, one existing R09 raw-proof PermissionError at /Users/korizatou114/Projects/とりにく工房ゲーム制作部/artifacts/original-completion-2026-10-02/p08-03-native-os-r09/index.json; no test weakened" + }, + { + "command": "python3 -m py_compile tools/check_p08_normal_placement_evidence.py tools/test_p08_normal_placement_evidence.py tests/fixtures/p08-03/checker_fixture.py", + "result": "exit 1; default macOS Python cache write PermissionError" + }, + { + "command": "PYTHONPYCACHEPREFIX=tests/fixtures/p08-03/.scratch/repair19-pycache python3 -m py_compile tools/check_p08_normal_placement_evidence.py tools/test_p08_normal_placement_evidence.py tests/fixtures/p08-03/checker_fixture.py", + "result": "exit 0" + }, + { + "command": "python3 -c 'import json; from pathlib import Path; [json.loads(Path(p).read_text()) for p in [\"docs/plans/original-completion-2026-10-02/execution/p08-03/implementation.json\", \"docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json\"]]; print(\"JSON load: PASS\")'", + "result": "exit 0; JSON load: PASS" + }, + { + "command": "git diff --check", + "result": "exit 0" + }, + { + "command": "python3 -c 'import subprocess,json,fnmatch; from pathlib import Path; allowed=json.loads(Path(\"docs/plans/original-completion-2026-10-02/execution/p08-03/REGISTERED_TASK.json\").read_text())[\"contract\"][\"allowed_paths\"]; paths=subprocess.check_output([\"git\",\"diff\",\"--name-only\",\"fd643f55a271e63c554b94681454d357117c0678\"],text=True).splitlines()+subprocess.check_output([\"git\",\"ls-files\",\"--others\",\"--exclude-standard\"],text=True).splitlines(); bad=[p for p in paths if not any(fnmatch.fnmatchcase(p,g) for g in allowed)]; print({\"paths\":paths,\"outside_allowed_paths\":bad}); assert not bad'", + "result": "exit 0; six changed files within allowed_paths; no untracked files after removing only generated repair19 py_compile cache" + } + ], + "remaining": [ + "Root inspects normal scrollbar operation on a short disposable diagnostic first, freezes final source, then runs a fresh contiguous long OS/native attempt", + "exactly one final all-suite regression on roster Root derives at final revision", + "independent Opus review of all five conditions with raw-proof access", + "integration, current final verification and ledger", + "Windows same-distribution/device remains open; no Mac substitution" + ], + "history": "Repair18 integrated API implementation and all prior history/pins preserved; no Runtime/product UI edit, no Godot/OS/native/full run, no git write or self-approval" + }, "status": "submitted", "acceptance_ids": [ "P08-03" diff --git a/tests/fixtures/p08-03/checker_fixture.py b/tests/fixtures/p08-03/checker_fixture.py index d50b74b..dd08541 100644 --- a/tests/fixtures/p08-03/checker_fixture.py +++ b/tests/fixtures/p08-03/checker_fixture.py @@ -183,6 +183,8 @@ def make(base, source, checker): runtime['command_queue']['processed'].append(copy.deepcopy(row)) return row + os_sequences = {} + def os_meta(row, pid): row['input_method'] = method if method != 'native_os': return @@ -200,11 +202,28 @@ def make(base, source, checker): requests.append({'action':'button','node':'/root/Main/ManualSaveButton'}) requests += [{'action':'wheel','ctrl':False,'move_mode':True}] requests += [{'action':'wheel','ctrl':False,'move_mode':False} for _ in range(18)] + if row['id'] == 'supported_connected': + geometry = {'target_rect':[120,0,20,20], 'target_screen_rect':[120,0,20,20], + 'palette_rect':[0,0,100,30], 'thumb_rect':[0,20,50,10], + 'viewport_to_screen':[1,0,0,1,0,0], + 'bar':{'rect':[0,20,100,10], 'visible':True, 'min_value':0, + 'max_value':200, 'page':100, 'value':0}} + requests = [dict(geometry, action='reveal_palette', node='/root/Main/Palette/Target', + direction='right', scroll_before=0), + {'action':'button','node':'/root/Main/Palette/Target'}] + requests row['requests'] = [] - for sequence, request in enumerate(requests, 1): - prefix = directory+'/'+row['id']+'-'+str(sequence)+'/' + for local_sequence, request in enumerate(requests, 1): + sequence = os_sequences.get(pid, 0)+1 + os_sequences[pid] = sequence + prefix = directory+'/'+row['id']+'-'+str(local_sequence)+'/' request.update(target_pid=pid, sequence=sequence) put(out/(prefix+'request.json'), request) + if request['action'] == 'reveal_palette': + after = copy.deepcopy(geometry) + after.update(target_rect=[60,0,20,20], target_screen_rect=[60,0,20,20], + thumb_rect=[30,20,50,10], scroll_after=60, visible_after=True) + after['bar']['value'] = 60 + put(out/(prefix+'reveal.json'), after) events = [{'synthetic':True}] if request['action'] == 'cell': events = [{'synthetic':True, 'button':1, 'pressed':True, diff --git a/tests/fixtures/p08-03/passive_placement.gd b/tests/fixtures/p08-03/passive_placement.gd index 3bfc95f..3e4664e 100644 --- a/tests/fixtures/p08-03/passive_placement.gd +++ b/tests/fixtures/p08-03/passive_placement.gd @@ -3,6 +3,10 @@ extends "res://tests/fixtures/p08-03/placement.gd" ## Shared parent contains the handler endpoints, but every such endpoint below ## is overridden; this class never emits UI signals or invokes product handlers. var receipts: Array = [] +var terminated := false +var terminal_record: Dictionary = {} +var terminal_cause_recorded := false +var reveal_rows: Array = [] var all_requests: Array = [] var input_observer: Node var last_dispatch: Dictionary = {} @@ -21,7 +25,29 @@ class InputObserver extends Node: row["dispatch"] = endpoint.dispatch_position(event.position) events.append(row) +func expect(ok: bool, message: String) -> void: + if terminated and ok: + return + if terminated and terminal_cause_recorded: + if not ok: + terminal_record["suppressed_after_terminal"] += 1 + json_file(role + "-terminal.json", terminal_record) + return + if terminated and not ok: + terminal_cause_recorded = true + super.expect(ok, message) + +func terminate(stem: String, reason: String) -> void: + terminated = true + terminal_record = {"pid": OS.get_process_id(), "role": role, "sequence": serial, + "stem": stem, "reason": reason, "suppressed_after_terminal": 0} + json_file(role + "-terminal.json", terminal_record) + expect(false, reason) + tree.quit(1) + func request(action: Dictionary) -> void: + if terminated: + return serial += 1 var stem := "%s-request-%04d" % [role, serial] var folder := directory.path_join(stem) @@ -40,13 +66,11 @@ func request(action: Dictionary) -> void: while not FileAccess.file_exists(path) and Time.get_ticks_msec() < deadline: await tree.process_frame if not FileAccess.file_exists(path): - expect(false, "Root CUA acknowledgement timeout") - tree.quit(1) + terminate(stem, "Root CUA acknowledgement timeout") return var ack: Variant = JSON.parse_string(FileAccess.get_file_as_string(path)) if not ack is Dictionary or ack.get("target_pid") != OS.get_process_id() or ack.get("sequence") != serial or ack.get("request_sha256") != FileAccess.get_sha256(folder.path_join("request.json")): - expect(false, "Root CUA acknowledgement identity/hash") - tree.quit(1) + terminate(stem, "Root CUA acknowledgement identity/hash") return await frames() for event: Dictionary in input_observer.events.slice(first_event): @@ -59,6 +83,8 @@ func request(action: Dictionary) -> void: all_requests.append("os-pair/" + stem + "/ack.json") func annotate(row: Dictionary) -> void: + row["reveal"] = reveal_rows.duplicate(true) + reveal_rows.clear() row["input_method"] = "native_os" row["requests"] = receipts.duplicate() receipts.clear() @@ -121,11 +147,57 @@ func save_initial_fixture() -> void: # Root loaded it using the normal menu. No model/setup mutation here. pass +func rect_values(rect: Rect2) -> Array: + return [rect.position.x, rect.position.y, rect.size.x, rect.size.y] + +func control_rect(node: Control, screen: bool = false) -> Rect2: + var transform := node.get_screen_transform() if screen else node.get_global_transform_with_canvas() + return transform * Rect2(Vector2.ZERO, node.size) + +func reveal_geometry(node: Control) -> Dictionary: + var bar: HScrollBar = main.palette_scroll.get_h_scroll_bar() + var bar_rect := control_rect(bar) + var span := bar.max_value - bar.min_value + var thumb := bar_rect + if span > 0: + thumb.size.x = bar_rect.size.x * bar.page / span + thumb.position.x += bar_rect.size.x * (bar.value - bar.min_value) / span + return {"target_rect": rect_values(control_rect(node)), "target_screen_rect": rect_values(control_rect(node, true)), + "palette_rect": rect_values(main.palette_scroll.get_global_rect()), + "bar": {"rect": rect_values(bar_rect), "visible": bar.is_visible_in_tree(), + "min_value": bar.min_value, "max_value": bar.max_value, "page": bar.page, "value": bar.value}, + "thumb_rect": rect_values(thumb), "viewport_to_screen": point(node)["viewport_to_screen"]} + func reveal(node: Control) -> void: - await request({"action": "reveal_palette", "node": String(node.get_path()), - "instructions": "Scroll the normal palette until this button is visible. Do not activate it yet."}) + if terminated: + return + var palette: Rect2 = main.palette_scroll.get_global_rect() + var target_rect := control_rect(node) + if palette.encloses(target_rect): + reveal_rows.append({"node": String(node.get_path()), "direction": "none"}) + return + var direction := "right" if target_rect.end.x > palette.end.x else "left" + var before: int = main.palette_scroll.scroll_horizontal + var action := reveal_geometry(node) + action.merge({"action": "reveal_palette", "node": String(node.get_path()), "direction": direction, + "scroll_before": before, "instructions": "Use the normal horizontal scrollbar to reveal this palette button without activating it."}) + await request(action) + if terminated: + return + await frames() + var after := reveal_geometry(node) + var visible_after: bool = main.palette_scroll.get_global_rect().encloses(control_rect(node)) + after.merge({"scroll_after": main.palette_scroll.scroll_horizontal, "visible_after": visible_after}) + var stem := "%s-request-%04d" % [role, serial] + json_file(stem + "/reveal.json", after) + reveal_rows.append(action) + var moved: bool = main.palette_scroll.scroll_horizontal > before if direction == "right" else main.palette_scroll.scroll_horizontal < before + if not visible_after or not moved: + terminate(stem, "palette target not visible after OS reveal") func button(node: BaseButton) -> void: + if terminated: + return expect(node != null and not node.disabled, "available production button") if node == null or node.disabled: return @@ -143,6 +215,8 @@ func button(node: BaseButton) -> void: input_observer = old_observer func click(cell: Vector3i) -> Dictionary: + if terminated: + return {} var view: WorldPreview = main.world_preview for attempt: int in range(8): if Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)): @@ -150,6 +224,8 @@ func click(cell: Vector3i) -> Dictionary: var framing := point(view) framing.merge({"action": "framing", "world_cell": vector(cell), "instructions": "Use ordinary zoom-out at the preview center until the requested world cell is visible; coordinates will be recomputed after ack."}) await request(framing) + if terminated: + return {} expect(Rect2(Vector2.ZERO, view.size).has_point(view.cell_to_canvas(cell)), "OS target visible after normal framing") var local := view.cell_to_canvas(cell) var viewport := view.get_global_transform_with_canvas() * local @@ -163,6 +239,8 @@ func click(cell: Vector3i) -> Dictionary: "guide_rect": [guide_rect.position.x, guide_rect.position.y, guide_rect.size.x, guide_rect.size.y], "guide_visible": main.opening_guide.visible, "instructions": "Click screen coordinates derived from viewport_to_screen (includes canvas stretch and window translation). Retain actual CUA coordinates and screenshots. If clipped or intercepted, recollect; never substitute fixed pixels."}) + if terminated: + return {} var selected: Array = last_dispatch.get("world_cell", []) expect(selected == vector(cell), "observed OS selected cell") var occupant: StringName = runtime.world_service.get_occupant(cell) @@ -202,18 +280,24 @@ func vector2(value: Vector2) -> Array: return [value.x, value.y] func wheel(event: InputEventMouseButton, move_mode: bool) -> void: + if terminated: + return var action := point(main.world_preview) action.merge({"action": "wheel", "direction": "up" if event.button_index == MOUSE_BUTTON_WHEEL_UP else "down", "ctrl": false, "move_mode": move_mode, "instructions": "Send one ordinary wheel step at the preview center without Ctrl; retain the CUA receipt."}) await request(action) func complete_build(id: StringName) -> void: + if terminated: + return await request({"action": "complete_construction", "definition": String(id), "minutes": runtime.definition_registry.get_facility(id).build_minutes, "instructions": "Use normal speed/play controls, wait for construction completion, then pause. No injected clock or model changes."}) expect(clock.paused, "Root paused after ordinary construction progression") func load_menu(menu: MenuButton) -> void: + if terminated: + return var action := point(menu) action.merge({"action": "load_menu", "item_id": 0, "instructions": "Open this normal menu and choose manual load (item 0)."}) await request(action) diff --git a/tools/check_p08_normal_placement_evidence.py b/tools/check_p08_normal_placement_evidence.py index 20fc0f5..cb8d1a0 100644 --- a/tools/check_p08_normal_placement_evidence.py +++ b/tools/check_p08_normal_placement_evidence.py @@ -901,6 +901,18 @@ def check(project_root, evidence_dir): require(os_proof['input_method'] == 'native_os' and os_proof['observer_mutations'] == [], 'fake OS provenance') os_pids = [os_phase(n) for n in os_proof['phases']] require(len(os_pids) == 2, 'OS separate PID pair') + role_actions = {} + for role, pid in zip(('writer', 'reader'), os_pids): + actions = [document(name) for name in index['artifacts'] + if name.startswith('os-pair/') and name.endswith('/request.json')] + actions = sorted((a for a in actions if a.get('target_pid') == pid), key=lambda a: a['sequence']) + terminal = 'os-pair/'+role+'-terminal.json' + if terminal in index['artifacts']: + stopped = document(terminal) + require(not any(a['sequence'] > stopped['sequence'] for a in actions), 'terminal OS continuation') + require(False, 'terminal OS failure') + require([a['sequence'] for a in actions] == list(range(1, len(actions)+1)), 'OS request sequence gap') + role_actions[pid] = actions os_rows = pair_gate(os_proof['directory'], os_pids, True) lifecycle = [] for role, pid in zip(('writer','reader'),os_pids): @@ -928,6 +940,30 @@ def check(project_root, evidence_dir): require(ack['target_pid'] == request['target_pid'] == observed['pid'] == expected_pid and ack['sequence'] == request['sequence'] == observed['sequence'], 'fake OS provenance') require(ack['request_sha256'] == sha(artifact(folder+'request.json')), 'OS ack request mismatch') + if request['action'] == 'reveal_palette': + def finite(values, size): + return isinstance(values, list) and len(values) == size and all( + isinstance(v, (int, float)) and not isinstance(v, bool) and math.isfinite(v) for v in values) + def geometry(data): + bar = data.get('bar', {}) + return (all(finite(data.get(k), 4) for k in ('target_rect', 'target_screen_rect', 'palette_rect', 'thumb_rect')) + and finite(data.get('viewport_to_screen'), 6) and isinstance(bar, dict) + and finite(bar.get('rect'), 4) and isinstance(bar.get('visible'), bool) + and finite([bar.get(k) for k in ('min_value', 'max_value', 'page', 'value')], 4)) + require(geometry(request) and finite([request.get('scroll_before')], 1) + and request.get('direction') in ('left', 'right'), 'unverified palette reveal') + require(folder+'reveal.json' in index['artifacts'], 'unverified palette reveal') + revealed = document(folder+'reveal.json') + require(geometry(revealed) and revealed.get('visible_after') is True + and finite([revealed.get('scroll_after')], 1), 'unverified palette reveal') + x,y,w,h = revealed['target_rect']; px,py,pw,ph = revealed['palette_rect'] + require(w >= 0 and h >= 0 and px <= x and py <= y and x+w <= px+pw and y+h <= py+ph, + 'unverified palette reveal') + delta = revealed['scroll_after'] - request['scroll_before'] + require(delta > 0 if request['direction'] == 'right' else delta < 0, 'palette reveal direction mismatch') + following = [a for a in role_actions[expected_pid] if a['sequence'] == request['sequence']+1] + require(len(following) == 1 and following[0]['action'] == 'button' + and following[0].get('node') == request.get('node') and request.get('node'), 'unverified palette reveal') raw = document(ack['input_receipt']) require(raw['method'] == 'native_os' and raw['target_pid'] == expected_pid and raw['timestamp_ns'] > 0 and raw['tool_name'] and raw['tool_call_id'], 'fake OS provenance') diff --git a/tools/test_p08_normal_placement_evidence.py b/tools/test_p08_normal_placement_evidence.py index c05ffe8..e862274 100644 --- a/tools/test_p08_normal_placement_evidence.py +++ b/tools/test_p08_normal_placement_evidence.py @@ -98,6 +98,60 @@ class EvidenceTests(unittest.TestCase): def test_positive_whole_checker(self): self.assertEqual([], C.check(self.root, self.out)) + def edit_os_request(self, folder, callback): + self.edit(folder+'/request.json', callback) + self.edit(folder+'/ack.json', lambda d: d.update(request_sha256=C.sha(self.out/(folder+'/request.json')))) + + def test_reveal_missing_thumb(self): + self.edit_os_request('os-pair/supported_connected-1', lambda d: d.pop('thumb_rect')) + self.reject('unverified palette reveal') + + def test_reveal_missing_bar(self): + self.edit_os_request('os-pair/supported_connected-1', lambda d: d.pop('bar')) + self.reject('unverified palette reveal') + + def test_reveal_not_visible(self): + self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(visible_after=False)) + self.reject('unverified palette reveal') + + def test_reveal_target_outside(self): + self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(target_rect=[110,0,20,20])) + self.reject('unverified palette reveal') + + def test_reveal_unchanged_scroll(self): + self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(scroll_after=0)) + self.reject('palette reveal direction mismatch') + + def test_reveal_opposite_scroll(self): + self.edit('os-pair/supported_connected-1/reveal.json', lambda d: d.update(scroll_after=-1)) + self.reject('palette reveal direction mismatch') + + def test_reveal_next_wrong_node(self): + self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(node='/other')) + self.reject('unverified palette reveal') + + def test_reveal_next_not_button(self): + self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(action='framing')) + self.reject('unverified palette reveal') + + def terminal(self, sequence): + F.put(self.out/'os-pair/writer-terminal.json', {'pid':303, 'role':'writer', + 'sequence':sequence, 'stem':'writer-request', 'reason':'timeout'}) + F.repin(self.out) + + def test_terminal_failure(self): + sequences = [C.load(p)['sequence'] for p in (self.out/'os-pair').glob('*/request.json')] + self.terminal(max(sequences)) + self.reject('terminal OS failure') + + def test_terminal_continuation(self): + self.terminal(1) + self.reject('terminal OS continuation') + + def test_os_sequence_gap(self): + self.edit_os_request('os-pair/supported_connected-2', lambda d: d.update(sequence=999)) + self.reject('OS request sequence gap') + def action_row(self, label): rows = C.load(self.out/'pair/writer-actions.json')['actions'] return copy.deepcopy(next(row for row in rows if row['id'] == label))