"""Pure delivery projection of independently recorded read-only preflight.""" from __future__ import annotations from datetime import datetime import re from backend.browser.preflight_policy import compile_preflight_policy _CLASSIFICATIONS = ("unique_dom_deep_reply", "needs_native_identity", "ambiguous", "unknown") _NAV_STATUSES = {"entered_note", "revisited_note", "excluded_note_blocked", "note_limit_blocked", "unknown_click_blocked", "action_not_allowed", "unexpected_note_rejected", "scope_history_untracked"} _NAV_ACTIONS = {"navigate", "click", "input", "send_keys", "scroll", "go_back", "wait", "done", "ask_user", "inspect_reply_relations", "advance_comments", "find_elements", "find_text", "search_page", "screenshot", "observe", "other"} def _valid_navigation(row: object) -> bool: if not isinstance(row, dict) or set(row) != {"id", "version", "run_id", "goal_version", "status", "action", "note_id", "observed_at"}: return False if (type(row["version"]) is not int or row["version"] != 1 or not isinstance(row["id"], str) or not re.fullmatch(r"preflight_nav_[a-f0-9]{32}", row["id"]) or not isinstance(row["run_id"], str) or not re.fullmatch(r"[a-f0-9]{32}", row["run_id"]) or type(row["goal_version"]) is not int or not 1 <= row["goal_version"] <= 1_000_000_000 or not isinstance(row["status"], str) or row["status"] not in _NAV_STATUSES or not isinstance(row["action"], str) or row["action"] not in _NAV_ACTIONS or not isinstance(row["observed_at"], str) or len(row["observed_at"]) > 40): return False note_id = row["note_id"] if note_id is not None and (not isinstance(note_id, str) or not re.fullmatch(r"[a-f0-9]{24}", note_id)): return False if row["status"] in {"entered_note", "revisited_note", "unexpected_note_rejected"} and note_id is None: return False try: datetime.fromisoformat(row["observed_at"]) except ValueError: return False return True def _run_context(task: dict) -> tuple[int, str | None]: inputs, artifacts = task.get("inputs") or {}, task.get("artifacts") or {} version = inputs.get("_goal_version", 1) if type(version) is not int or version < 1: return 0, None marker = artifacts.get("preflight_run") if (not isinstance(marker, dict) or set(marker) != {"run_id", "goal_version"} or marker.get("goal_version") != version or type(marker.get("goal_version")) is not int or not isinstance(marker.get("run_id"), str) or not re.fullmatch(r"[a-f0-9]{32}", marker["run_id"])): return version, None return version, marker["run_id"] def summarize_preflight(task: dict) -> dict | None: """Count only the current goal/run, without changing or upgrading history.""" from backend.browser.tools import _valid_preflight_telemetry inputs, artifacts = task.get("inputs") or {}, task.get("artifacts") or {} policy = compile_preflight_policy(inputs.get("goal", ""), inputs.get("max_notes")) if not policy.active: return None goal_version, run_id = _run_context(task) def current(row): return isinstance(row, dict) and run_id is not None and row.get("run_id") == run_id and row.get("goal_version") == goal_version raw_observations = artifacts.get("preflight_observations") raw_navigation = artifacts.get("preflight_navigation") observations = raw_observations if isinstance(raw_observations, list) else [] navigation = raw_navigation if isinstance(raw_navigation, list) else [] valid_observations = [row for row in observations if current(row) and _valid_preflight_telemetry(row)] valid_navigation = [row for row in navigation if current(row) and _valid_navigation(row)] prior_navigation = [row for row in navigation if _valid_navigation(row)] malformed_current = (any(current(row) and not _valid_preflight_telemetry(row) for row in observations) or any(current(row) and not _valid_navigation(row) for row in navigation)) # Wrapper receipts are append-only. A later unknown observation supersedes # an earlier visible state; repeated checks never multiply sample counts. latest = {} for row in valid_observations: latest[row["note_id"]] = row visited = list(dict.fromkeys(row["note_id"] for row in valid_navigation if row["status"] in {"entered_note", "revisited_note"})) all_observed_notes = set(visited) | set(latest) order = [*visited, *(note_id for note_id in latest if note_id not in visited)] counts = dict.fromkeys(_CLASSIFICATIONS, 0) inspected, not_in_view, unknown, folded, folded_unknown = 0, 0, 0, 0, 0 checked = eligible = 0 notes = [] for ordinal, note_id in enumerate(order, 1): row = latest.get(note_id) confirmed = bool(row and row["status"] in {"checked", "no_visible_reply"} and row["discussion_state"] == "visible_comments") if confirmed: inspected += 1 checked += row["checked_count"] eligible += row["eligible_count"] for key in counts: counts[key] += row["classification_counts"][key] elif row and row["discussion_state"] == "comments_not_in_view": not_in_view += 1 else: unknown += 1 if row and row["has_unexpanded_replies"] is True: folded += 1 elif not row or row["has_unexpanded_replies"] is None: folded_unknown += 1 notes.append({"ordinal": ordinal, "inspected": confirmed, "status": "inspected" if confirmed else "not_in_view" if row and row["discussion_state"] == "comments_not_in_view" else "unknown", "checked_count": row["checked_count"] if confirmed else 0, "eligible_count": row["eligible_count"] if confirmed else 0, "has_unexpanded_replies": row["has_unexpanded_replies"] if row else None}) actual_violation = (any(row["status"] == "unexpected_note_rejected" for row in prior_navigation) or bool(all_observed_notes & set(policy.excluded_note_ids)) or policy.max_notes is not None and len(all_observed_notes) > policy.max_notes) truncated = any(row["history_truncated"] for row in valid_observations) unknown_scope = (not valid_navigation or not visited or malformed_current or truncated or bool(set(latest) - set(visited)) or any(row["status"] == "scope_history_untracked" for row in prior_navigation)) visited_known = bool(run_id and valid_navigation and visited and not malformed_current and not any(row["status"] == "scope_history_untracked" for row in valid_navigation)) unknown_clicks = sum(row["status"] == "unknown_click_blocked" for row in valid_navigation) scope_passed = False if actual_violation else None if unknown_scope else True history = artifacts.get("browser_history") or {} browser_success = artifacts.get("browser_verdict") == "success" and history.get("is_successful") is not False completed = bool(browser_success and inspected > 0 and not not_in_view and not unknown and scope_passed is True) if task.get("status") == "running": headline = "正在预检可见回复关系" elif completed: headline = "预检已完成,发现可继续取证的样本" if eligible else "预检已完成,可见范围未发现合格样本" elif inspected: headline = "预检阶段结果已保留" else: headline = "尚未形成可核对的预检结果" detail = (f"本段已记录实际访问{len(visited)}篇。" if visited_known else "本段实际访问数量未记录,访问范围无法完整核对。") detail += (f"可确认预检{inspected}篇;检查当前可见回复{checked}条,其中{eligible}条具备继续取证的依据。" if valid_observations else "本段尚无独立、可确认的预检记录。") if not_in_view: detail += f"{not_in_view}篇讨论尚未进入视口,不计入已预检。" if unknown: detail += f"{unknown}篇尚无可确认的当前预检结果。" if folded: detail += f"{folded}篇仍有折叠回复未展开。" if folded_unknown: detail += f"{folded_unknown}篇的折叠状态尚未确认。" if unknown_clicks: detail += f"已拒绝{unknown_clicks}个目标无法事前核对的入口,相关范围仍保留限制。" if scope_passed is False: detail += "已观察到超出本次访问范围的页面,本轮不能认定完成。" elif scope_passed is None: detail += "访问范围记录仍不完整,不能认定已遵守全部范围。" detail += "零合格样本只对应本次可见范围,未展开或未加载部分仍未知;本轮未生成候选名单。" return {"version": 1, "completed": completed, "inspected_notes": inspected, "visited_notes": len(visited), "run_available": run_id is not None, "recorded": bool(valid_observations), "visited_known": visited_known, "checked_count": checked, "eligible_count": eligible, "classification_counts": counts, "not_in_view_notes": not_in_view, "unknown_notes": unknown, "folded_notes": folded, "folded_unknown_notes": folded_unknown, "scope_passed": scope_passed, "history_truncated": truncated, "max_notes": policy.max_notes, "unknown_click_blocked_count": unknown_clicks, "headline": headline, "detail": detail, "notes": notes} def delivery_projection(task: dict) -> dict | None: summary = summarize_preflight(task) if summary is None: return None artifacts = task.get("artifacts") or {} history = artifacts.get("browser_history") or {} running = task.get("status") == "running" status = "in_progress" if running else "fulfilled" if summary["completed"] else "partial" if summary["inspected_notes"] else "no_evidence" return {"version": 1, "status": status, "headline": summary["headline"], "preflight": summary, "runtime": {"status": task.get("status") or "unknown", "step": task.get("current_step"), "stop_reason": artifacts.get("stop_reason"), "stop_detail": artifacts.get("stop_detail") or task.get("pause_reason") or "", "raw_browser_success": history.get("is_successful"), "browser_verdict": artifacts.get("browser_verdict")}, "fulfillment": {"scope_met": summary["scope_passed"] is True, "requirements_met": summary["inspected_notes"] > 0 and not summary["not_in_view_notes"] and not summary["unknown_notes"], "complete": summary["completed"], "assessment_current": True, "target": None, "current": 0, "remaining": None}, "artifacts": {"list_available": False, "file_required": False, "file_available": False, "delivered": summary["inspected_notes"] > 0, "export_count": 0}, "counts": dict.fromkeys(("eligible", "known_identity", "supplemental", "unresolved", "signals", "verified_accounts"), 0), "conditions": [ {"key": "preflight_visible_discussion", "label": "实际预检当前可见讨论", "required": True, "met": summary["inspected_notes"] > 0 and not summary["not_in_view_notes"] and not summary["unknown_notes"], "current": summary["inspected_notes"], "target": None}, {"key": "preflight_visit_scope", "label": "实际访问符合本次范围", "required": True, "met": summary["scope_passed"] is True, "current": summary["visited_notes"], "target": None}], "people": []}