Keep the last in-template copy on main so history can restore it; product continues in jiangchang-ui-recorder. Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
5.2 KiB
Python
134 lines
5.2 KiB
Python
"""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
|