"""Static, non-mutating replay readiness report for a finished capture.""" from __future__ import annotations import json from pathlib import Path from typing import Any _INTERACTIVE = {"click", "fill", "type", "press_key", "select", "toggle"} def _read(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) def build_replay_preflight(flow_dir: Path) -> dict[str, Any]: """Assess whether recorded actions contain executable, verified locators. This deliberately does not click or navigate. `event_time_unique` means the locator count was measured in the event-origin document during capture; it is not represented as a cross-session replay result. """ root = Path(flow_dir) actions = sorted( (_read(path) for path in (root / "actions").glob("*.json")), key=lambda item: int(item.get("sequence") or 0), ) elements = { item.get("id"): item for item in (_read(path) for path in (root / "elements").glob("*.json")) if item.get("id") } rows: list[dict[str, Any]] = [] for action in actions: action_type = str(action.get("action_type") or "") if action_type not in _INTERACTIVE: rows.append({ "action_id": action.get("id"), "sequence": action.get("sequence"), "action_type": action_type, "result": "context_only", "reason": "non_interactive_or_navigation", }) continue element_id = action.get("target_element_id") element = elements.get(element_id) or {} primary = dict(element.get("primary_locator") or {}) eligible = bool(primary.get("production_eligible")) unique = primary.get("unique") is True score = float(primary.get("stability_score") or 0.0) result = "pass" if eligible and unique and score >= 0.55 else "warning" if not eligible or not unique: result = "fail" target = dict(action.get("target_snapshot") or {}) bounds = dict(target.get("bounds") or target.get("rect") or {}) area = float(bounds.get("width") or 0) * float(bounds.get("height") or 0) oversized_click = action_type == "click" and area > 1_000_000 root_window_target = ( action_type == "click" and str(target.get("control_type") or "") == "WindowControl" ) if oversized_click or root_window_target: result = "fail" validation = str(primary.get("validation") or "none") verification = ( "event_time_unique" if validation == "live" and unique else "snapshot_unique" if validation in {"snapshot_only", "live_tree"} and unique else "not_validated" ) raw_meta = dict((action.get("raw_event") or {}).get("meta") or {}) rows.append({ "action_id": action.get("id"), "sequence": action.get("sequence"), "action_type": action_type, "result": result, "element_id": element_id, "logical_name": element.get("logical_name") or "", "locator": primary.get("expression") or "", "strategy": primary.get("strategy") or "", "stability_score": score, "match_count": primary.get("actual_count"), "verification": verification, "page_id": raw_meta.get("page_id") or "", "document_id": raw_meta.get("document_id") or "", "raw_event_ids": list(action.get("raw_event_ids") or []), "reason": ( "oversized_or_root_window_click_target" if oversized_click or root_window_target else "" ), }) counts = { name: sum(1 for row in rows if row.get("result") == name) for name in ("pass", "warning", "fail", "context_only") } from diagnostics.capture_validator import validate_capture quality = validate_capture(root) blocking_codes = { "TEXT_INPUT_TRANSACTION_MISSING", "CLICK_TARGET_GENERIC_CONTAINER", "KEY_TARGET_ROOT_WINDOW", "CLICK_TARGET_OVERSIZED", "ACTION_TARGET_MISSING", "ZERO_ACTIONS", } quality_blockers = [ issue for issue in quality.get("issues") or [] if issue.get("severity") == "error" or issue.get("code") in blocking_codes ] counts["quality_blockers"] = len(quality_blockers) return { "schema": "openclaw.ui_inspector.replay_preflight.v1", "mode": "static_non_mutating", "note": "事件时刻唯一性不等同于跨会话真实回放;本报告不会点击、输入或导航。", "flow_dir": str(root), "ready": not quality_blockers and counts.get("fail", 0) == 0, "summary": counts, "quality_gate": { "passed": not quality_blockers, "issues": quality_blockers, }, "actions": rows, } def write_replay_preflight(flow_dir: Path) -> dict[str, Any]: report = build_replay_preflight(flow_dir) path = Path(flow_dir) / "diagnostics" / "replay_report.json" path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") return report